/**
 * EncryptID Session Management
 *
 * Handles session tokens, cross-app SSO, and authentication levels.
 * This is Layer 4/5 of the EncryptID architecture.
 */

import { AuthenticationResult, bufferToBase64url } from './webauthn';
import { resetLinkedWalletStore } from './linked-wallets';

const ENCRYPTID_SERVER = ''; // same-origin — avoids cross-origin issues on Safari

// ============================================================================
// TYPES
// ============================================================================

/**
 * Authentication security levels
 */
export enum AuthLevel {
  /** Session token only - read-only, public content */
  BASIC = 1,

  /** Recent WebAuthn (within 15 min) - standard operations */
  STANDARD = 2,

  /** Fresh WebAuthn (just authenticated) - sensitive operations */
  ELEVATED = 3,

  /** Fresh WebAuthn + explicit consent - critical operations */
  CRITICAL = 4,
}

/**
 * EncryptID session token claims
 */
export interface EncryptIDClaims {
  // Standard JWT claims
  iss: string;           // Issuer: https://encryptid.online
  sub: string;           // Subject: DID (did:key:z6Mk...)
  aud: string | string[];// Audience: authorized apps
  iat: number;           // Issued at
  exp: number;           // Expiration (short-lived: 15 min)
  jti: string;           // JWT ID (for revocation)

  // Identity claims (set by server)
  username: string;      // Display username
  did: string;           // did:key:... derived from credential

  // EncryptID-specific claims
  eid: {
    walletAddress?: string;        // AA wallet address if deployed
    credentialId: string;          // WebAuthn credential ID
    authLevel: AuthLevel;          // Security level of this session
    authTime: number;              // When WebAuthn was performed
    capabilities: {
      encrypt: boolean;            // Has derived encryption key
      sign: boolean;               // Has derived signing key
      wallet: boolean;             // Can authorize wallet ops
    };
    recoveryConfigured: boolean;   // Has social recovery set up
  };
}

/**
 * Session state stored locally
 */
export interface SessionState {
  accessToken: string;
  refreshToken: string;
  claims: EncryptIDClaims;
  lastAuthTime: number;
}

/**
 * Operation permission requirements
 */
export interface OperationPermission {
  minAuthLevel: AuthLevel;
  requiresCapability?: 'encrypt' | 'sign' | 'wallet';
  maxAgeSeconds?: number;  // Max time since last WebAuthn
}

// ============================================================================
// OPERATION PERMISSIONS
// ============================================================================

/**
 * Permission requirements for various operations across r-apps
 */
export const OPERATION_PERMISSIONS: Record<string, OperationPermission> = {
  // rspace operations
  'rspace:view-public': { minAuthLevel: AuthLevel.BASIC },
  'rspace:view-private': { minAuthLevel: AuthLevel.STANDARD },
  'rspace:edit-board': { minAuthLevel: AuthLevel.STANDARD },
  'rspace:create-board': { minAuthLevel: AuthLevel.STANDARD },
  'rspace:delete-board': { minAuthLevel: AuthLevel.ELEVATED, maxAgeSeconds: 300 },
  'rspace:encrypt-board': { minAuthLevel: AuthLevel.ELEVATED, requiresCapability: 'encrypt' },

  // rwallet operations
  'rwallet:view-balance': { minAuthLevel: AuthLevel.BASIC },
  'rwallet:view-history': { minAuthLevel: AuthLevel.STANDARD },
  'rwallet:send-small': { minAuthLevel: AuthLevel.STANDARD, requiresCapability: 'wallet' },
  'rwallet:send-large': { minAuthLevel: AuthLevel.ELEVATED, requiresCapability: 'wallet', maxAgeSeconds: 60 },
  'rwallet:add-guardian': { minAuthLevel: AuthLevel.CRITICAL, maxAgeSeconds: 60 },
  'rwallet:remove-guardian': { minAuthLevel: AuthLevel.CRITICAL, maxAgeSeconds: 60 },
  'rwallet:link-wallet': { minAuthLevel: AuthLevel.ELEVATED, maxAgeSeconds: 300 },
  'rwallet:unlink-wallet': { minAuthLevel: AuthLevel.ELEVATED, maxAgeSeconds: 300 },
  'rwallet:external-send': { minAuthLevel: AuthLevel.ELEVATED, requiresCapability: 'wallet', maxAgeSeconds: 60 },

  // rvote operations
  'rvote:view-proposals': { minAuthLevel: AuthLevel.BASIC },
  'rvote:cast-vote': { minAuthLevel: AuthLevel.ELEVATED, requiresCapability: 'sign', maxAgeSeconds: 300 },
  'rvote:delegate': { minAuthLevel: AuthLevel.ELEVATED, requiresCapability: 'wallet' },

  // rfiles operations
  'rfiles:list-files': { minAuthLevel: AuthLevel.STANDARD },
  'rfiles:download-own': { minAuthLevel: AuthLevel.STANDARD, requiresCapability: 'encrypt' },
  'rfiles:upload': { minAuthLevel: AuthLevel.STANDARD, requiresCapability: 'encrypt' },
  'rfiles:share': { minAuthLevel: AuthLevel.ELEVATED, requiresCapability: 'encrypt' },
  'rfiles:delete': { minAuthLevel: AuthLevel.ELEVATED, maxAgeSeconds: 300 },
  'rfiles:export-keys': { minAuthLevel: AuthLevel.CRITICAL, maxAgeSeconds: 60 },

  // rmaps operations
  'rmaps:view-public': { minAuthLevel: AuthLevel.BASIC },
  'rmaps:add-location': { minAuthLevel: AuthLevel.STANDARD },
  'rmaps:edit-location': { minAuthLevel: AuthLevel.STANDARD, requiresCapability: 'sign' },

  // Payment operations (x402 and Safe treasury)
  'payment:x402': { minAuthLevel: AuthLevel.STANDARD, requiresCapability: 'wallet' },
  'payment:safe-propose': { minAuthLevel: AuthLevel.ELEVATED, requiresCapability: 'wallet', maxAgeSeconds: 60 },
  'payment:safe-execute': { minAuthLevel: AuthLevel.CRITICAL, requiresCapability: 'wallet', maxAgeSeconds: 60 },

  // Account operations
  'account:view-profile': { minAuthLevel: AuthLevel.STANDARD },
  'account:edit-profile': { minAuthLevel: AuthLevel.ELEVATED },
  'account:export-data': { minAuthLevel: AuthLevel.CRITICAL, maxAgeSeconds: 60 },
  'account:delete': { minAuthLevel: AuthLevel.CRITICAL, maxAgeSeconds: 60 },
};

// ============================================================================
// SESSION MANAGER
// ============================================================================

const SESSION_STORAGE_KEY = 'encryptid_session';
const TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000;  // 5 minutes before expiry

/**
 * EncryptID Session Manager
 *
 * Handles session state, token refresh, and permission checks.
 */
export class SessionManager {
  private session: SessionState | null = null;
  private refreshTimer: ReturnType<typeof setTimeout> | null = null;

  constructor() {
    // Try to restore session from storage
    this.restoreSession();
  }

  /**
   * Initialize session from authentication result.
   * Exchanges the credential for a server-signed JWT via EncryptID.
   */
  async createSession(
    authResult: AuthenticationResult,
    did: string,
    capabilities: EncryptIDClaims['eid']['capabilities']
  ): Promise<SessionState> {
    const now = Math.floor(Date.now() / 1000);

    // Get a server-signed JWT by exchanging the credential via EncryptID
    let accessToken: string;
    try {
      // Step 1: Get a server challenge
      const startRes = await fetch(`${ENCRYPTID_SERVER}/api/auth/start`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ credentialId: authResult.credentialId }),
      });
      if (!startRes.ok) throw new Error(`Failed to start server auth: ${startRes.status}`);
      const startBody = await startRes.json().catch(() => { throw new Error('Invalid JSON from auth/start'); });
      const { options } = startBody;

      // Step 2: Complete auth with credentialId to get signed token
      const completeRes = await fetch(`${ENCRYPTID_SERVER}/api/auth/complete`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          challenge: options.challenge,
          credential: { credentialId: authResult.credentialId },
        }),
      });
      if (!completeRes.ok) throw new Error(`Server auth failed: ${completeRes.status}`);
      const data = await completeRes.json().catch(() => { throw new Error('Invalid JSON from auth/complete'); });
      if (!data.token) throw new Error('No token in response');
      accessToken = data.token;
    } catch (err) {
      console.warn('EncryptID: Server token exchange failed, using local token', err);
      // Fallback to unsigned token if server is unreachable
      accessToken = this.createUnsignedToken({
        iss: 'https://auth.ridentity.online',
        sub: did,
        aud: 'rspace.online',
        iat: now,
        exp: now + 15 * 60,
        jti: bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer),
        username: did.slice(0, 16),
        did,
        eid: {
          credentialId: authResult.credentialId,
          authLevel: AuthLevel.ELEVATED,
          authTime: now,
          capabilities,
          recoveryConfigured: false,
        },
      });
    }

    // Decode claims from the token (works for both signed and unsigned JWTs)
    let claims: EncryptIDClaims;
    try {
      const payloadB64 = accessToken.split('.')[1];
      claims = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')));
    } catch {
      // Fallback claims
      claims = {
        iss: 'https://auth.ridentity.online',
        sub: did,
        aud: 'rspace.online',
        iat: now,
        exp: now + 15 * 60,
        jti: bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer),
        username: did.slice(0, 16),
        did,
        eid: {
          credentialId: authResult.credentialId,
          authLevel: AuthLevel.ELEVATED,
          authTime: now,
          capabilities,
          recoveryConfigured: false,
        },
      };
    }

    this.session = {
      accessToken,
      refreshToken: accessToken,  // Server token serves as both
      claims,
      lastAuthTime: Date.now(),
    };

    // Store session
    this.persistSession();

    // Schedule token refresh
    this.scheduleRefresh();

    console.log('EncryptID: Session created', {
      did: (claims.sub || did).slice(0, 30) + '...',
      authLevel: AuthLevel[claims.eid?.authLevel ?? AuthLevel.ELEVATED],
      signed: !accessToken.endsWith('.'),
    });

    return this.session;
  }

  /**
   * Get current session
   */
  getSession(): SessionState | null {
    return this.session;
  }

  /**
   * Get current DID
   */
  getDID(): string | null {
    return this.session?.claims.sub ?? null;
  }

  /**
   * Get current auth level
   */
  getAuthLevel(): AuthLevel {
    if (!this.session) return AuthLevel.BASIC;

    // Check if session is still valid
    const now = Math.floor(Date.now() / 1000);
    if (now >= this.session.claims.exp) {
      return AuthLevel.BASIC;
    }

    // Check auth age for level downgrade
    const authAge = now - this.session.claims.eid.authTime;

    if (authAge < 60) {
      return AuthLevel.ELEVATED;  // Within 1 minute
    } else if (authAge < 15 * 60) {
      return AuthLevel.STANDARD;  // Within 15 minutes
    } else {
      return AuthLevel.BASIC;
    }
  }

  /**
   * Check if user can perform an operation
   */
  canPerform(operation: string): { allowed: boolean; reason?: string } {
    const permission = OPERATION_PERMISSIONS[operation];

    if (!permission) {
      return { allowed: false, reason: 'Unknown operation' };
    }

    if (!this.session) {
      return { allowed: false, reason: 'Not authenticated' };
    }

    const currentLevel = this.getAuthLevel();

    // Check auth level
    if (currentLevel < permission.minAuthLevel) {
      return {
        allowed: false,
        reason: `Requires ${AuthLevel[permission.minAuthLevel]} auth level (current: ${AuthLevel[currentLevel]})`,
      };
    }

    // Check capability
    if (permission.requiresCapability) {
      const hasCapability = this.session.claims.eid.capabilities[permission.requiresCapability];
      if (!hasCapability) {
        return {
          allowed: false,
          reason: `Requires ${permission.requiresCapability} capability`,
        };
      }
    }

    // Check max age
    if (permission.maxAgeSeconds) {
      const authAge = Math.floor(Date.now() / 1000) - this.session.claims.eid.authTime;
      if (authAge > permission.maxAgeSeconds) {
        return {
          allowed: false,
          reason: `Authentication too old (${authAge}s > ${permission.maxAgeSeconds}s)`,
        };
      }
    }

    return { allowed: true };
  }

  /**
   * Require fresh authentication for an operation
   */
  requiresFreshAuth(operation: string): boolean {
    const permission = OPERATION_PERMISSIONS[operation];
    if (!permission) return true;

    if (permission.minAuthLevel >= AuthLevel.CRITICAL) return true;
    if (permission.maxAgeSeconds && permission.maxAgeSeconds <= 60) return true;

    return false;
  }

  /**
   * Upgrade auth level after fresh WebAuthn
   */
  upgradeAuthLevel(level: AuthLevel = AuthLevel.ELEVATED): void {
    if (!this.session) return;

    this.session.claims.eid.authLevel = level;
    this.session.claims.eid.authTime = Math.floor(Date.now() / 1000);
    this.session.lastAuthTime = Date.now();

    this.persistSession();

    console.log('EncryptID: Auth level upgraded to', AuthLevel[level]);
  }

  /**
   * Clear session (logout).
   * Notifies the server so all other browser sessions are revoked on their next refresh.
   */
  clearSession(): void {
    // Fire-and-forget server logout so other sessions get revoked
    if (this.session?.accessToken) {
      fetch(`${ENCRYPTID_SERVER}/api/session/logout`, {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${this.session.accessToken}` },
      }).catch(() => { /* best-effort */ });
    }

    this.session = null;

    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer);
      this.refreshTimer = null;
    }

    // Clear linked wallet cache so it doesn't survive across user sessions
    resetLinkedWalletStore();

    try {
      localStorage.removeItem(SESSION_STORAGE_KEY);
    } catch {
      // Ignore storage errors
    }

    console.log('EncryptID: Session cleared');
  }

  /**
   * Check if session is valid
   */
  isValid(): boolean {
    if (!this.session) return false;

    const now = Math.floor(Date.now() / 1000);
    return now < this.session.claims.exp;
  }

  // ==========================================================================
  // PRIVATE METHODS
  // ==========================================================================

  private createUnsignedToken(claims: EncryptIDClaims): string {
    // In production, this would be a proper JWT signed by the server
    // For the prototype, we create a base64-encoded JSON token
    const header = { alg: 'none', typ: 'JWT' };
    const headerB64 = btoa(JSON.stringify(header));
    const payloadB64 = btoa(JSON.stringify(claims));
    return `${headerB64}.${payloadB64}.`;
  }

  private createRefreshToken(did: string): string {
    // In production, refresh tokens would be opaque server-issued tokens
    const payload = {
      sub: did,
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,  // 7 days
      jti: bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer),
    };
    return btoa(JSON.stringify(payload));
  }

  private persistSession(): void {
    if (!this.session) return;

    try {
      localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(this.session));
    } catch (error) {
      console.warn('EncryptID: Failed to persist session', error);
    }
  }

  private restoreSession(): void {
    try {
      const stored = localStorage.getItem(SESSION_STORAGE_KEY);
      if (stored) {
        const session = JSON.parse(stored) as SessionState;

        // Verify session is not expired
        const now = Math.floor(Date.now() / 1000);
        if (now < session.claims.exp) {
          this.session = session;
          this.scheduleRefresh();
          console.log('EncryptID: Session restored');

          // Validate with server — detects logout from another session
          this.validateWithServer();
        } else {
          // Session expired, clear it
          localStorage.removeItem(SESSION_STORAGE_KEY);
        }
      }
    } catch (error) {
      console.warn('EncryptID: Failed to restore session', error);
    }
  }

  /** Check with the server that this session hasn't been revoked (e.g. logout from another browser) */
  private async validateWithServer(): Promise<void> {
    if (!this.session) return;

    try {
      const res = await fetch(`${ENCRYPTID_SERVER}/api/session/verify`, {
        headers: { 'Authorization': `Bearer ${this.session.accessToken}` },
      });

      if (!res.ok) {
        const data = await res.json().catch(() => ({}));
        if (data.error === 'Session revoked' || res.status === 401) {
          console.warn('EncryptID: Session revoked by another session, logging out');
          // Clear locally without hitting the server logout endpoint again
          this.session = null;
          if (this.refreshTimer) {
            clearTimeout(this.refreshTimer);
            this.refreshTimer = null;
          }
          resetLinkedWalletStore();
          try { localStorage.removeItem(SESSION_STORAGE_KEY); } catch { /* ignore */ }

          // Notify the app so UI updates (dispatched on window for global listeners)
          window.dispatchEvent(new CustomEvent('encryptid:session-revoked'));
        }
      }
    } catch {
      // Network error — don't log out, let the token expire naturally
    }
  }

  private scheduleRefresh(): void {
    if (!this.session) return;

    // Clear existing timer
    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer);
    }

    // Calculate time until refresh needed
    const expiresAt = this.session.claims.exp * 1000;
    const refreshAt = expiresAt - TOKEN_REFRESH_THRESHOLD;
    const delay = Math.max(refreshAt - Date.now(), 0);

    this.refreshTimer = setTimeout(() => {
      this.refreshTokens();
    }, delay);
  }

  private async refreshTokens(): Promise<void> {
    if (!this.session) return;

    try {
      // Call EncryptID server to refresh the token
      const res = await fetch(`${ENCRYPTID_SERVER}/api/session/refresh`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.session.accessToken}`,
          'Content-Type': 'application/json',
        },
      });

      if (!res.ok) throw new Error(`Refresh failed: ${res.status}`);
      const data = await res.json().catch(() => { throw new Error('Invalid JSON from session/refresh'); });
      if (!data.token) throw new Error('No token in refresh response');

      // Decode new claims
      const payloadB64 = data.token.split('.')[1];
      const claims = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')));

      this.session.accessToken = data.token;
      this.session.refreshToken = data.token;
      this.session.claims = claims;

      this.persistSession();
      this.scheduleRefresh();

      console.log('EncryptID: Tokens refreshed (server-signed)');
    } catch (err) {
      console.warn('EncryptID: Token refresh failed', err);
      // Session will expire naturally; user will need to re-authenticate
    }
  }
}

// ============================================================================
// SINGLETON INSTANCE
// ============================================================================

let sessionManagerInstance: SessionManager | null = null;

/**
 * Get the global session manager instance
 */
export function getSessionManager(): SessionManager {
  if (!sessionManagerInstance) {
    sessionManagerInstance = new SessionManager();
  }
  return sessionManagerInstance;
}
