import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { ApiTokenDoc } from '../db/index.js';
import type {
  IApiTokenPolicy,
  IStoredApiToken,
  IApiTokenInfo,
  TApiTokenScope,
} from '../../ts_interfaces/data/route-management.js';

const TOKEN_PREFIX_STR = 'dcr_';
const ENV_ADMIN_TOKEN_ID = 'env-admin-token';
const ENV_ADMIN_TOKEN_CREATED_BY = 'dcrouter-env';

export class ApiTokenManager {
  private tokens = new Map<string, IStoredApiToken>();

  constructor() {}

  public async initialize(): Promise<void> {
    await this.loadTokens();
    await this.ensureEnvAdminToken();
    if (this.tokens.size > 0) {
      logger.log('info', `Loaded ${this.tokens.size} API token(s) from storage`);
    }
  }

  // =========================================================================
  // Token lifecycle
  // =========================================================================

  /**
   * Create a new API token. Returns the raw token value (shown once).
   */
  public async createToken(
    name: string,
    scopes: TApiTokenScope[],
    expiresInDays: number | null,
    createdBy: string,
    policy?: IApiTokenPolicy,
  ): Promise<{ id: string; rawToken: string }> {
    const id = plugins.uuid.v4();
    const randomBytes = plugins.crypto.randomBytes(32);
    const rawPayload = `${id}:${randomBytes.toString('base64url')}`;
    const rawToken = `${TOKEN_PREFIX_STR}${rawPayload}`;

    const tokenHash = this.hashToken(rawToken);

    const now = Date.now();
    const stored: IStoredApiToken = {
      id,
      name,
      tokenHash,
      scopes,
      policy,
      createdAt: now,
      expiresAt: expiresInDays != null ? now + expiresInDays * 86400000 : null,
      lastUsedAt: null,
      createdBy,
      enabled: true,
    };

    this.tokens.set(id, stored);
    await this.persistToken(stored);
    logger.log('info', `API token '${name}' created (id: ${id})`);
    return { id, rawToken };
  }

  /**
   * Validate a raw token string. Returns the stored token if valid, null otherwise.
   * Also updates lastUsedAt.
   */
  public async validateToken(rawToken: string): Promise<IStoredApiToken | null> {
    if (!rawToken.startsWith(TOKEN_PREFIX_STR)) return null;

    const hash = this.hashToken(rawToken);

    for (const stored of this.tokens.values()) {
      if (stored.tokenHash === hash) {
        if (!stored.enabled) return null;
        if (stored.expiresAt !== null && stored.expiresAt < Date.now()) return null;

        // Update lastUsedAt (fire and forget)
        stored.lastUsedAt = Date.now();
        this.persistToken(stored).catch(() => {});
        return stored;
      }
    }
    return null;
  }

  /**
   * Check if a token has a specific scope.
   */
  public hasScope(token: IStoredApiToken, scope: TApiTokenScope): boolean {
    if (token.policy?.role === 'admin') return true;

    const isGatewayClientToken = token.policy?.role === 'gatewayClient';
    const gatewayClientAllowedScopes = new Set<TApiTokenScope>([
      'gateway-clients:read',
      'gateway-clients:write',
      'workhosters:read',
      'workhosters:write',
    ]);
    if (isGatewayClientToken && !gatewayClientAllowedScopes.has(scope)) {
      return false;
    }

    if (!isGatewayClientToken && token.scopes.includes('*')) return true;

    const scopes = new Set<TApiTokenScope>([...token.scopes, ...(token.policy?.scopes || [])]);
    if (scopes.has(scope)) return true;

    const equivalentScopes: Partial<Record<TApiTokenScope, TApiTokenScope[]>> = {
      'gateway-clients:read': ['workhosters:read'],
      'gateway-clients:write': ['workhosters:write'],
      'workhosters:read': ['gateway-clients:read'],
      'workhosters:write': ['gateway-clients:write'],
    };
    return Boolean(equivalentScopes[scope]?.some((alias) => scopes.has(alias)));
  }

  /**
   * List all tokens (safe info only, no hashes).
   */
  public listTokens(): IApiTokenInfo[] {
    const result: IApiTokenInfo[] = [];
    for (const stored of this.tokens.values()) {
      result.push({
        id: stored.id,
        name: stored.name,
        scopes: stored.scopes,
        policy: stored.policy,
        createdAt: stored.createdAt,
        expiresAt: stored.expiresAt,
        lastUsedAt: stored.lastUsedAt,
        enabled: stored.enabled,
      });
    }
    return result;
  }

  /**
   * Revoke (delete) a token.
   */
  public async revokeToken(id: string): Promise<boolean> {
    if (!this.tokens.has(id)) return false;
    const token = this.tokens.get(id)!;
    this.tokens.delete(id);
    const doc = await ApiTokenDoc.findById(id);
    if (doc) await doc.delete();
    logger.log('info', `API token '${token.name}' revoked (id: ${id})`);
    return true;
  }

  /**
   * Roll (regenerate) a token's secret while keeping its identity.
   * Returns the new raw token value (shown once).
   */
  public async rollToken(id: string): Promise<{ id: string; rawToken: string } | null> {
    const stored = this.tokens.get(id);
    if (!stored) return null;

    const randomBytes = plugins.crypto.randomBytes(32);
    const rawPayload = `${id}:${randomBytes.toString('base64url')}`;
    const rawToken = `${TOKEN_PREFIX_STR}${rawPayload}`;

    stored.tokenHash = this.hashToken(rawToken);
    await this.persistToken(stored);
    logger.log('info', `API token '${stored.name}' rolled (id: ${id})`);
    return { id, rawToken };
  }

  /**
   * Enable or disable a token.
   */
  public async toggleToken(id: string, enabled: boolean): Promise<boolean> {
    const stored = this.tokens.get(id);
    if (!stored) return false;
    stored.enabled = enabled;
    await this.persistToken(stored);
    logger.log('info', `API token '${stored.name}' ${enabled ? 'enabled' : 'disabled'} (id: ${id})`);
    return true;
  }

  // =========================================================================
  // Private
  // =========================================================================

  private async loadTokens(): Promise<void> {
    const docs = await ApiTokenDoc.findAll();
    for (const doc of docs) {
      if (doc.id) {
        this.tokens.set(doc.id, {
          id: doc.id,
          name: doc.name,
          tokenHash: doc.tokenHash,
          scopes: doc.scopes,
          policy: doc.policy,
          createdAt: doc.createdAt,
          expiresAt: doc.expiresAt,
          lastUsedAt: doc.lastUsedAt,
          createdBy: doc.createdBy,
          enabled: doc.enabled,
        });
      }
    }
  }

  private async ensureEnvAdminToken(): Promise<void> {
    const rawToken = process.env.DCROUTER_ADMIN_API_TOKEN?.trim();
    if (!rawToken) return;

    if (!rawToken.startsWith(TOKEN_PREFIX_STR)) {
      throw new Error(`DCROUTER_ADMIN_API_TOKEN must start with ${TOKEN_PREFIX_STR}`);
    }
    if (rawToken.length < TOKEN_PREFIX_STR.length + 32) {
      throw new Error('DCROUTER_ADMIN_API_TOKEN is too short');
    }

    const now = Date.now();
    const existing = this.tokens.get(ENV_ADMIN_TOKEN_ID);
    const stored: IStoredApiToken = {
      id: ENV_ADMIN_TOKEN_ID,
      name: process.env.DCROUTER_ADMIN_API_TOKEN_NAME?.trim() || 'Environment Admin Token',
      tokenHash: this.hashToken(rawToken),
      scopes: ['*'],
      policy: { role: 'admin' },
      createdAt: existing?.createdAt || now,
      expiresAt: null,
      lastUsedAt: existing?.lastUsedAt || null,
      createdBy: existing?.createdBy || ENV_ADMIN_TOKEN_CREATED_BY,
      enabled: true,
    };

    this.tokens.set(stored.id, stored);
    await this.persistToken(stored);
    logger.log('info', `Environment admin API token ensured (id: ${stored.id})`);
  }

  private hashToken(rawToken: string): string {
    return plugins.crypto.createHash('sha256').update(rawToken).digest('hex');
  }

  private async persistToken(stored: IStoredApiToken): Promise<void> {
    const existing = await ApiTokenDoc.findById(stored.id);
    if (existing) {
      existing.name = stored.name;
      existing.tokenHash = stored.tokenHash;
      existing.scopes = stored.scopes;
      existing.policy = stored.policy;
      existing.createdAt = stored.createdAt;
      existing.expiresAt = stored.expiresAt;
      existing.lastUsedAt = stored.lastUsedAt;
      existing.createdBy = stored.createdBy;
      existing.enabled = stored.enabled;
      await existing.save();
    } else {
      const doc = new ApiTokenDoc();
      doc.id = stored.id;
      doc.name = stored.name;
      doc.tokenHash = stored.tokenHash;
      doc.scopes = stored.scopes;
      doc.policy = stored.policy;
      doc.createdAt = stored.createdAt;
      doc.expiresAt = stored.expiresAt;
      doc.lastUsedAt = stored.lastUsedAt;
      doc.createdBy = stored.createdBy;
      doc.enabled = stored.enabled;
      await doc.save();
    }
  }
}
