/**
 * Enterprise key parsing — turn an array of opaque key strings into a
 * structured list of marketplace sources for sync / registry to consume.
 *
 * Two key formats:
 *   short:  `<tenant_id>:<github_pat>`      → looked up via KNOWN_TENANTS
 *   long:   `<github_pat>@<repo_url>`       → tenant_id derived from repo
 *
 * Pure parsing — does no I/O. Persistence lives in lib/settings.ts; HTTP
 * lives in lib/connectors/sync.ts.
 */

import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import { lookupTenant } from './enterprise-known';
import { encryptSecret, decryptSecret, isEncrypted } from './crypto';
import { getEnterpriseKeysPath } from './dirs';

export interface EnterpriseSource {
  tenant_id: string;
  display_name: string;
  repo_url: string;
  github_pat: string;
  priority: number;
}

export interface ParseResult {
  sources: EnterpriseSource[];
  errors: { key_preview: string; reason: string }[];
}

const SHORT_RE = /^([a-z0-9_-]+):(.+)$/i;
const LONG_RE = /^(.+)@(github\.com\/[A-Za-z0-9_.\/-]+)$/;

function preview(key: string): string {
  if (key.length <= 12) return key.slice(0, 4) + '…';
  return key.slice(0, 6) + '…' + key.slice(-4);
}

export function parseKey(key: string, priority: number): EnterpriseSource | { error: string } {
  // Strip every whitespace + zero-width char — pastes from Teams/email/
  // wiki often carry trailing \r\n, line-wraps, or invisible separators.
  // Valid keys have no internal whitespace (PAT alphanumeric + '_',
  // repo URL has no spaces).
  const trimmed = key.replace(/[\s\u200B-\u200F\u202A-\u202E\u2060\uFEFF]/g, '');
  if (!trimmed) return { error: 'empty' };

  const longMatch = trimmed.match(LONG_RE);
  if (longMatch) {
    const [, pat, repo_url] = longMatch;
    const tenant_id = deriveTenantFromRepo(repo_url);
    return {
      tenant_id,
      display_name: tenant_id,
      repo_url,
      github_pat: pat,
      priority,
    };
  }

  const shortMatch = trimmed.match(SHORT_RE);
  if (shortMatch) {
    const [, tenant_id, pat] = shortMatch;
    const known = lookupTenant(tenant_id);
    if (!known) {
      return { error: `unknown tenant '${tenant_id}' — use long form '<pat>@<repo_url>' or update KNOWN_TENANTS` };
    }
    return {
      tenant_id: known.tenant_id,
      display_name: known.display_name,
      repo_url: known.repo_url,
      github_pat: pat,
      priority,
    };
  }

  return { error: 'unrecognized format — expected `<tenant>:<pat>` or `<pat>@<repo_url>`' };
}

export function parseKeys(keys: string[]): ParseResult {
  const sources: EnterpriseSource[] = [];
  const errors: ParseResult['errors'] = [];
  const seenTenants = new Set<string>();

  keys.forEach((key, idx) => {
    const result = parseKey(key, idx);
    if ('error' in result) {
      errors.push({ key_preview: preview(key), reason: result.error });
      return;
    }
    // Defensive dedup: even if `.enterprise-keys.json` somehow has two
    // entries for the same tenant (e.g. legacy pre-dedup persist), the
    // marketplace still surfaces only one source. Earlier index wins
    // — matches how priority works for non-dup tenants.
    if (seenTenants.has(result.tenant_id)) return;
    seenTenants.add(result.tenant_id);
    sources.push(result);
  });

  return { sources, errors };
}

function deriveTenantFromRepo(repo_url: string): string {
  const parts = repo_url.split('/');
  const last = parts[parts.length - 1] || 'unknown';
  const m = last.match(/forge-enterprise-agent-(.+)/);
  return m ? m[1] : last;
}

// ───────────────────────── persistence ─────────────────────────
// Keys are stored as an encrypted JSON array at <dataDir>/.enterprise-keys.json.
// The on-disk shape is { v: 1, keys: ["enc:...", "enc:..."] } so we can
// rotate the encryption format later without breaking existing files.

interface KeyFile {
  v: 1;
  keys: string[];  // each entry is encryptSecret(rawKey)
}

function readKeyFile(): KeyFile {
  const path = getEnterpriseKeysPath();
  if (!existsSync(path)) return { v: 1, keys: [] };
  try {
    const raw = readFileSync(path, 'utf-8');
    const parsed = JSON.parse(raw);
    if (!parsed || !Array.isArray(parsed.keys)) return { v: 1, keys: [] };
    return { v: 1, keys: parsed.keys };
  } catch {
    return { v: 1, keys: [] };
  }
}

function writeKeyFile(file: KeyFile): void {
  const path = getEnterpriseKeysPath();
  const dir = dirname(path);
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  writeFileSync(path, JSON.stringify(file, null, 2), { mode: 0o600 });
}

/** Return raw key strings (decrypted) in priority order. */
export function getEnterpriseKeys(): string[] {
  const file = readKeyFile();
  return file.keys.map(k => isEncrypted(k) ? decryptSecret(k) : k).filter(Boolean);
}

/** Replace the full key list. Order = priority (index 0 = highest). */
export function setEnterpriseKeys(rawKeys: string[]): void {
  const file: KeyFile = {
    v: 1,
    keys: rawKeys.filter(k => k && k.trim()).map(k => encryptSecret(k.trim())),
  };
  writeKeyFile(file);
}

/** Append a key (returns false if a key with the same tenant_id already exists). */
export function addEnterpriseKey(rawKey: string): { ok: true; tenant_id: string } | { ok: false; reason: string } {
  const parsed = parseKey(rawKey, 0);
  if ('error' in parsed) return { ok: false, reason: parsed.error };

  const existing = getEnterpriseKeys();
  const existingSources = parseKeys(existing).sources;
  if (existingSources.some(s => s.tenant_id === parsed.tenant_id)) {
    return { ok: false, reason: `tenant '${parsed.tenant_id}' already configured — remove it first` };
  }

  setEnterpriseKeys([...existing, rawKey]);
  return { ok: true, tenant_id: parsed.tenant_id };
}

/**
 * Replace the key for a tenant_id with a new raw key. The new key's
 * parsed tenant_id must match the target (otherwise it's an add, not an
 * update — callers should remove+add explicitly).
 */
export function updateEnterpriseKey(
  tenant_id: string,
  rawKey: string,
): { ok: true } | { ok: false; reason: string } {
  const parsed = parseKey(rawKey, 0);
  if ('error' in parsed) return { ok: false, reason: parsed.error };
  if (parsed.tenant_id !== tenant_id) {
    return { ok: false, reason: `key resolves to tenant '${parsed.tenant_id}', not '${tenant_id}' — remove the existing key first` };
  }
  const existing = getEnterpriseKeys();
  let found = false;
  const next = existing.map(k => {
    const r = parseKey(k, 0);
    if ('error' in r || r.tenant_id !== tenant_id) return k;
    found = true;
    return rawKey;
  });
  if (!found) return { ok: false, reason: `no key for tenant '${tenant_id}'` };
  setEnterpriseKeys(next);
  return { ok: true };
}

/** Remove all keys for a tenant_id. Returns true if anything was removed. */
export function removeEnterpriseKey(tenant_id: string): boolean {
  const existing = getEnterpriseKeys();
  const filtered = existing.filter(k => {
    const r = parseKey(k, 0);
    return 'error' in r ? true : r.tenant_id !== tenant_id;
  });
  if (filtered.length === existing.length) return false;
  setEnterpriseKeys(filtered);
  return true;
}

/** Convenience: parsed + ordered source list for sync / registry to consume. */
export function listEnterpriseSources(): EnterpriseSource[] {
  return parseKeys(getEnterpriseKeys()).sources;
}
