import fs from 'node:fs';
import path from 'node:path';
import type { ApplicationConfig, StoredAppKey, Channel } from '@zyphon/shared-types';
import {
  generateApiKey,
  computeKeyVerifier,
  verifyApiKey,
  getMaskedApiKeyPrefix,
} from '@zyphon/crypto';
import type { QuotaManager } from './quota-manager.js';

export interface AppValidationResult {
  valid: boolean;
  code?: 'UNAUTHENTICATED' | 'APP_DISABLED' | 'CHANNEL_FORBIDDEN' | 'RATE_LIMITED' | 'DAILY_QUOTA_EXCEEDED';
  message?: string;
  app?: ApplicationConfig;
}

export class ApplicationRegistry {
  private apps = new Map<string, ApplicationConfig>();
  private appKeys = new Map<string, StoredAppKey[]>(); // app_id -> StoredAppKey[]
  private stateFilePath: string;
  private pepper: string;
  private quotaManager: QuotaManager;

  constructor(
    initialApps: ApplicationConfig[] = [],
    quotaManager: QuotaManager,
    stateDir = './data/state',
    pepper = process.env.ZYPHON_SYSTEM_PEPPER || 'zyphon_default_system_pepper_entropy_salt'
  ) {
    this.quotaManager = quotaManager;
    this.pepper = pepper;
    const resolvedStateDir = path.resolve(stateDir);
    if (!fs.existsSync(resolvedStateDir)) {
      fs.mkdirSync(resolvedStateDir, { recursive: true });
    }
    this.stateFilePath = path.join(resolvedStateDir, 'app-keys.json');

    this.setApplications(initialApps);
    this.loadKeys();
  }

  public setApplications(apps: ApplicationConfig[]): void {
    this.apps.clear();
    for (const app of apps) {
      this.apps.set(app.id, app);
    }
  }

  public getApplication(id: string): ApplicationConfig | undefined {
    return this.apps.get(id);
  }

  public getAllApplications(): ApplicationConfig[] {
    return Array.from(this.apps.values());
  }

  private loadKeys(): void {
    if (!fs.existsSync(this.stateFilePath)) return;

    try {
      const raw = fs.readFileSync(this.stateFilePath, 'utf8');
      const data: Record<string, StoredAppKey[]> = JSON.parse(raw);
      this.appKeys.clear();
      for (const [appId, keys] of Object.entries(data)) {
        this.appKeys.set(appId, keys);
      }
    } catch {
      // Corrupt state fallback
    }
  }

  private saveKeys(): void {
    const obj = Object.fromEntries(this.appKeys.entries());
    const tmpPath = `${this.stateFilePath}.tmp.${Date.now()}`;
    try {
      fs.writeFileSync(tmpPath, JSON.stringify(obj, null, 2), { encoding: 'utf8', mode: 0o600 });
      fs.renameSync(tmpPath, this.stateFilePath);
      fs.chmodSync(this.stateFilePath, 0o600);
    } catch {
      try {
        if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
      } catch {}
    }
  }

  /**
   * Generates a new API key for an application.
   * Returns the plaintext key once (cannot be retrieved later).
   */
  public generateKey(appId: string): { app_id: string; api_key: string; key_prefix: string } {
    const rawKey = generateApiKey();
    const hash = computeKeyVerifier(rawKey, this.pepper);
    const keyPrefix = getMaskedApiKeyPrefix(rawKey);

    const storedKey: StoredAppKey = {
      app_id: appId,
      key_prefix: keyPrefix,
      hash,
      created_at: Date.now(),
    };

    let keys = this.appKeys.get(appId);
    if (!keys) {
      keys = [];
      this.appKeys.set(appId, keys);
    }
    keys.push(storedKey);
    this.saveKeys();

    return {
      app_id: appId,
      api_key: rawKey,
      key_prefix: keyPrefix,
    };
  }

  /**
   * List registered key metadata (prefixes and creation dates, never secrets)
   */
  public getAppKeySummaries(appId: string): Array<{ key_prefix: string; created_at: number; expires_at?: number }> {
    const keys = this.appKeys.get(appId) || [];
    return keys.map((k) => ({
      key_prefix: k.key_prefix,
      created_at: k.created_at,
      expires_at: k.expires_at,
    }));
  }

  /**
   * Revoke an API key by prefix
   */
  public revokeKey(appId: string, keyPrefix: string): boolean {
    const keys = this.appKeys.get(appId);
    if (!keys) return false;

    const filtered = keys.filter((k) => k.key_prefix !== keyPrefix);
    if (filtered.length !== keys.length) {
      this.appKeys.set(appId, filtered);
      this.saveKeys();
      return true;
    }
    return false;
  }

  /**
   * Authenticate a provided API key against all stored application keys.
   */
  public authenticateKey(rawKey: string): ApplicationConfig | null {
    if (!rawKey || !rawKey.startsWith('zy_')) {
      return null;
    }

    for (const [appId, keys] of this.appKeys.entries()) {
      for (const k of keys) {
        if (k.expires_at && Date.now() > k.expires_at) {
          continue; // Expired key
        }
        if (verifyApiKey(rawKey, k.hash, this.pepper)) {
          return this.apps.get(appId) || null;
        }
      }
    }

    return null;
  }

  /**
   * Validate application status, channel authorization, and quotas.
   */
  public validateDispatch(app: ApplicationConfig, channel: Channel): AppValidationResult {
    // 1. Verify app enabled
    if (!app.enabled) {
      return {
        valid: false,
        code: 'APP_DISABLED',
        message: `Application '${app.id}' is currently disabled`,
        app,
      };
    }

    // 2. Channel authorization (fail-closed, no wildcards allowed)
    if (!app.channels.includes(channel)) {
      return {
        valid: false,
        code: 'CHANNEL_FORBIDDEN',
        message: `Application '${app.id}' is not authorized to use channel '${channel}'`,
        app,
      };
    }

    // 3. Sliding minute rate limit check
    if (app.quota.per_min > 0) {
      const allowed = this.quotaManager.checkMinuteLimit(app.id, app.quota.per_min);
      if (!allowed) {
        return {
          valid: false,
          code: 'RATE_LIMITED',
          message: `Rate limit of ${app.quota.per_min} requests/minute exceeded for application '${app.id}'`,
          app,
        };
      }
    }

    // 4. Daily quota check
    if (app.quota.per_day > 0) {
      const allowed = this.quotaManager.checkAndIncrementDailyLimit(app.id, app.quota.per_day);
      if (!allowed) {
        return {
          valid: false,
          code: 'DAILY_QUOTA_EXCEEDED',
          message: `Daily quota of ${app.quota.per_day} messages exceeded for application '${app.id}'`,
          app,
        };
      }
    }

    return { valid: true, app };
  }
}

