import type Database from 'better-sqlite3';
import { encrypt, decrypt, loadKeyFromEnv } from './crypto.js';
import type {
  AuthKind,
  McpServerPublic,
  McpServerRecord,
  McpDiscoveryMetadata,
} from './types.js';
import { logger } from '../logger.js';

const ID_REGEX = /^[a-z0-9_-]{1,64}$/;
// RFC 7230 header field-name token chars. Guards against header injection
// (CR/LF, colon, spaces) when a custom auth header name is supplied.
const HEADER_NAME_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;

export interface UpsertInput {
  id: string;
  name: string;
  url: string;
  authKind: AuthKind;
  ownerId: string | null;  // null = global/admin-managed
  // OAuth fields (required when authKind === 'oauth')
  oauthClientId?: string;
  oauthClientSecret?: string;
  oauthScopes?: string | null;
  // API key field (required when authKind === 'api_key')
  staticToken?: string;
  // Optional custom auth header name for api_key servers (e.g. 'xc-mcp-token').
  authHeaderName?: string | null;
  enabled?: boolean;
  createdBy?: string | null;
}

interface Row {
  id: string;
  name: string;
  url: string;
  oauth_client_id: string;
  oauth_client_secret_enc: Buffer;
  oauth_scopes: string | null;
  issuer: string | null;
  authorization_endpoint: string | null;
  token_endpoint: string | null;
  discovery_fingerprint: string | null;
  enabled: number;
  created_by: string | null;
  created_at: string;
  updated_at: string;
  // Phase 8 columns
  auth_kind: string;
  static_token_enc: Buffer | null;
  owner_id: string | null;
  auth_header_name: string | null;
}

function rowToPublic(r: Row): McpServerPublic {
  return {
    id: r.id,
    name: r.name,
    url: r.url,
    authKind: (r.auth_kind ?? 'oauth') as AuthKind,
    ownerId: r.owner_id ?? null,
    authHeaderName: r.auth_header_name ?? null,
    oauthClientId: r.oauth_client_id,
    oauthScopes: r.oauth_scopes,
    issuer: r.issuer,
    authorizationEndpoint: r.authorization_endpoint,
    tokenEndpoint: r.token_endpoint,
    discoveryFingerprint: r.discovery_fingerprint,
    enabled: r.enabled === 1,
    createdBy: r.created_by,
    createdAt: r.created_at,
    updatedAt: r.updated_at,
  };
}

export function createRegistry(db: Database.Database) {
  return {
    upsert(input: UpsertInput): void {
      if (!ID_REGEX.test(input.id)) {
        throw new Error(`id must match ${ID_REGEX}: got "${input.id}"`);
      }
      const key = loadKeyFromEnv();
      const authKind = input.authKind;

      // Validate per-kind requirements
      if (authKind === 'oauth') {
        if (!input.oauthClientId || !input.oauthClientSecret) {
          throw new Error(`authKind 'oauth' requires oauthClientId and oauthClientSecret`);
        }
      } else if (authKind === 'api_key') {
        if (!input.staticToken) {
          throw new Error(`authKind 'api_key' requires staticToken`);
        }
      } else {
        throw new Error(`Unknown authKind: ${authKind as string}`);
      }

      // Optional custom auth header name (api_key only). Validated against the
      // RFC 7230 token grammar to prevent header injection.
      const authHeaderName = input.authHeaderName?.trim() || null;
      if (authHeaderName !== null) {
        if (authKind !== 'api_key') {
          throw new Error(`authHeaderName is only valid for authKind 'api_key'`);
        }
        if (!HEADER_NAME_REGEX.test(authHeaderName)) {
          throw new Error(`authHeaderName must be a valid HTTP header token: got "${authHeaderName}"`);
        }
      }

      // For oauth: encrypt the real secret; static_token_enc stays NULL.
      // For api_key: oauth_client_id = '' and oauth_client_secret_enc = encrypt('', key)
      //   because those columns are NOT NULL in the original schema. They are never read
      //   for api_key servers. static_token_enc = encrypt(realToken, key).
      const oauthClientId = authKind === 'oauth' ? input.oauthClientId! : '';
      const oauthSecretEnc = encrypt(authKind === 'oauth' ? input.oauthClientSecret! : '', key);
      const staticTokenEnc = authKind === 'api_key' ? encrypt(input.staticToken!, key) : null;

      db.prepare(
        `INSERT INTO mcp_servers
         (id, name, url, oauth_client_id, oauth_client_secret_enc, oauth_scopes,
          auth_kind, owner_id, static_token_enc, auth_header_name, enabled, created_by)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
         ON CONFLICT(id) DO UPDATE SET
           name=excluded.name,
           url=excluded.url,
           oauth_client_id=excluded.oauth_client_id,
           oauth_client_secret_enc=excluded.oauth_client_secret_enc,
           oauth_scopes=excluded.oauth_scopes,
           auth_kind=excluded.auth_kind,
           owner_id=excluded.owner_id,
           static_token_enc=excluded.static_token_enc,
           auth_header_name=excluded.auth_header_name,
           enabled=excluded.enabled,
           updated_at=datetime('now')`,
      ).run(
        input.id,
        input.name,
        input.url,
        oauthClientId,
        oauthSecretEnc,
        input.oauthScopes ?? null,
        authKind,
        input.ownerId ?? null,
        staticTokenEnc,
        authHeaderName,
        input.enabled === false ? 0 : 1,
        input.createdBy ?? null,
      );
      logger.info(`[mcp:registry] upsert server id=${input.id} url=${input.url} authKind=${authKind}`);
    },

    delete(id: string): void {
      db.prepare('DELETE FROM mcp_servers WHERE id = ?').run(id);
      logger.info(`[mcp:registry] delete server id=${id}`);
    },

    listPublic(): McpServerPublic[] {
      const rows = db.prepare('SELECT * FROM mcp_servers ORDER BY id').all() as Row[];
      return rows.map(rowToPublic);
    },

    /**
     * Returns all enabled servers visible to the given user:
     * - Global (owner_id IS NULL) servers are visible to everyone.
     * - User-owned (owner_id = userId) servers are only visible to that user.
     */
    listEnabledForUser(userId: string): McpServerPublic[] {
      const rows = db
        .prepare('SELECT * FROM mcp_servers WHERE enabled = 1 AND (owner_id IS NULL OR owner_id = ?) ORDER BY id')
        .all(userId) as Row[];
      return rows.map(rowToPublic);
    },

    /**
     * Returns only servers owned by the given user (for owner/admin scoped listing).
     */
    listEnabledForOwner(ownerId: string): McpServerPublic[] {
      const rows = db
        .prepare('SELECT * FROM mcp_servers WHERE enabled = 1 AND owner_id = ? ORDER BY id')
        .all(ownerId) as Row[];
      return rows.map(rowToPublic);
    },

    /** @deprecated Use listEnabledForUser(userId) to include user-owned servers. */
    listEnabledPublic(): McpServerPublic[] {
      const rows = db
        .prepare('SELECT * FROM mcp_servers WHERE enabled = 1 ORDER BY id')
        .all() as Row[];
      return rows.map(rowToPublic);
    },

    getDecrypted(id: string): McpServerRecord | null {
      const row = db.prepare('SELECT * FROM mcp_servers WHERE id = ?').get(id) as
        | Row
        | undefined;
      if (!row) return null;
      const key = loadKeyFromEnv();
      const authKind = (row.auth_kind ?? 'oauth') as AuthKind;
      const oauthClientSecret = authKind === 'oauth'
        ? decrypt(row.oauth_client_secret_enc, key)
        : ''; // not used for api_key servers
      const staticToken = (authKind === 'api_key' && row.static_token_enc)
        ? decrypt(row.static_token_enc, key)
        : null;
      return {
        ...rowToPublic(row),
        oauthClientSecret,
        staticToken,
      };
    },

    setDiscovery(id: string, meta: McpDiscoveryMetadata): void {
      db.prepare(
        `UPDATE mcp_servers
         SET issuer=?, authorization_endpoint=?, token_endpoint=?, discovery_fingerprint=?, updated_at=datetime('now')
         WHERE id=?`,
      ).run(meta.issuer, meta.authorizationEndpoint, meta.tokenEndpoint, meta.fingerprint, id);
    },

    setEnabled(id: string, enabled: boolean): void {
      db.prepare(
        `UPDATE mcp_servers SET enabled=?, updated_at=datetime('now') WHERE id=?`,
      ).run(enabled ? 1 : 0, id);
    },
  };
}

export type McpRegistry = ReturnType<typeof createRegistry>;
