/**
 * POST /api/connectors/import-config-template
 *
 * Two phases driven by the request body:
 *
 *   multipart/form-data with `file` (JSON) → analyze:
 *     Parse template, scan for ${key} placeholders, return a deduplicated
 *     list of prompts the user still needs to answer. Same key referenced
 *     by multiple connectors (e.g. gitlab_pat in both gitlab.token and
 *     jenkins.instances[0].gitlab_pat) is asked once.
 *
 *   application/json `{ template, values }` → apply:
 *     Substitute ${key} with the user-supplied values, then merge each
 *     connector row into connector-configs.json. Existing non-empty
 *     fields are preserved — the template never overwrites a real token.
 *
 * Template shape (see ~/MyDocuments/obsidian-project/30-Resources/forge/
 * connector-configs.template.json):
 *   {
 *     "_README": "...",
 *     "_prompts": { "gitlab_pat": {label, hint, secret, required}, ... },
 *     "<connector_id>": { "config": {...}, "enabled": bool },
 *     ...
 *   }
 * Keys starting with "_" are metadata, not connectors.
 */

import { NextResponse } from 'next/server';
import { join } from 'node:path';
import { getDataDir } from '@/lib/dirs';
import { loadSettings } from '@/lib/settings';
import {
  getConnector,
  getInstalledConnector,
  setConnectorConfig,
  setConnectorEnabled,
} from '@/lib/connectors/registry';
import { installFromRegistry } from '@/lib/connectors/sync';
import { resolveWizardTemplate } from '@/lib/connectors/wizard-template';
import { decryptDeep } from '@/lib/enterprise-secret';
import type { AgentEntry } from '@/lib/settings';
import { saveSettings } from '@/lib/settings';
// Bundled fallback — shipped with Forge so the "Import Template" button
// works out of the box without any extra setup. The shared resolver
// handles the user-override + enterprise + public tiers; this stays as
// the final safety net when none of those have a cached template.
import bundledTemplate from '@/templates/connector-config-template.json';

const LOCAL_TEMPLATE_FILE = 'config-template.json';

/**
 * Resolve template source through the cross-marketplace resolver, then
 * fall back to the bundled JSON. The legacy `source` tag (`local | bundled`)
 * is broadened to also surface `enterprise-<tenant>` / `public` so the
 * UI can tell users which source provided the wizard.
 */
function resolveTemplate(): { source: string; template: any; path: string | null } {
  const resolved = resolveWizardTemplate();
  if (resolved) {
    // Map the resolver's 'user' tag back to the legacy 'local' name so
    // existing UI that switches on it keeps working. Enterprise / public
    // pass through unchanged.
    const sourceTag = resolved.source === 'user' ? 'local' : resolved.source;
    const path = resolved.source === 'user' ? join(getDataDir(), LOCAL_TEMPLATE_FILE) : null;
    return { source: sourceTag, template: resolved.template, path };
  }
  return { source: 'bundled', template: bundledTemplate, path: null };
}

interface PromptDef {
  label: string;
  hint?: string;
  /** Optional link to the page where the user creates/finds this value. */
  url?: string;
  /** Display label for the link; defaults to "Open token page" in the UI. */
  url_label?: string;
  secret?: boolean;
  required?: boolean;
  /** UI grouping override. Defaults to the first-target connector id when
   *  unset. Use this when a prompt is semantically tied to connector A
   *  (e.g. gitlab token-name) but only USED inside connector B's config
   *  (e.g. jenkins.instances[0].username) — wizard groups it under A. */
  group?: string;
  /** Pre-filled value the wizard shows in the input by default. User can
   *  still edit. Useful for "instance name = default-jenkins" type fields. */
  default?: string;
}

interface PendingPrompt extends PromptDef {
  key: string;
  targets: Array<{ connector: string; field_path: string }>;
}

const PLACEHOLDER_RE = /\$\{([a-zA-Z0-9_]+)\}/g;

// ─── Helpers ──────────────────────────────────────────────

function isMetaKey(k: string): boolean {
  return k.startsWith('_');
}

/** Walk a value, collect every ${key} usage with its source path. */
function scanPlaceholders(
  value: unknown,
  path: string,
  out: Map<string, Set<string>>,
): void {
  if (typeof value === 'string') {
    const matches = value.matchAll(PLACEHOLDER_RE);
    for (const m of matches) {
      const key = m[1];
      if (!out.has(key)) out.set(key, new Set());
      out.get(key)!.add(path);
    }
    return;
  }
  if (Array.isArray(value)) {
    value.forEach((v, i) => scanPlaceholders(v, `${path}[${i}]`, out));
    return;
  }
  if (value && typeof value === 'object') {
    for (const [k, v] of Object.entries(value)) {
      scanPlaceholders(v, path ? `${path}.${k}` : k, out);
    }
  }
}

/** Replace every ${key} in a string with values[key]. Unmapped keys → empty string. */
function substituteString(s: string, values: Record<string, string>): string {
  return s.replace(PLACEHOLDER_RE, (_, key) => values[key] ?? '');
}

function substituteAll(value: unknown, values: Record<string, string>): unknown {
  if (typeof value === 'string') return substituteString(value, values);
  if (Array.isArray(value)) return value.map(v => substituteAll(v, values));
  if (value && typeof value === 'object') {
    const out: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(value)) out[k] = substituteAll(v, values);
    return out;
  }
  return value;
}

function isEffectivelyEmpty(v: unknown): boolean {
  if (v == null) return true;
  if (typeof v === 'string') return v.trim() === '';
  if (Array.isArray(v)) return v.length === 0;
  if (typeof v === 'object') return Object.keys(v as object).length === 0;
  return false;
}

// ─── Analyze ──────────────────────────────────────────────

function analyze(template: any): {
  pending: PendingPrompt[];
  static_apply_targets: string[];
  missing_manifests: string[];
  agents_preview?: Array<{ id: string; tool?: string; has_secret: boolean }>;
} {
  const promptsDict: Record<string, PromptDef> = template._prompts || {};

  // Agents preview — what the import will add to settings.agents
  const agentsPreview: Array<{ id: string; tool?: string; has_secret: boolean }> = [];
  if (template._agents && typeof template._agents === 'object') {
    for (const [agentId, raw] of Object.entries(template._agents as Record<string, any>)) {
      const env = (raw?.env ?? {}) as Record<string, unknown>;
      const has_secret = Object.values(env).some((v) => typeof v === 'string' && v.startsWith('ent:'));
      agentsPreview.push({ id: agentId, tool: raw?.tool, has_secret });
    }
  }

  // Map: placeholder_key → Set of "connector.field_path"
  const usage = new Map<string, Set<string>>();
  const staticTargets: string[] = [];
  const missing: string[] = [];

  for (const [id, row] of Object.entries(template)) {
    if (isMetaKey(id)) continue;
    if (!getConnector(id)) { missing.push(id); continue; }
    staticTargets.push(id);
    scanPlaceholders((row as any).config, id, usage);
  }

  const pending: PendingPrompt[] = [];
  for (const [key, paths] of usage.entries()) {
    const def = promptsDict[key] || { label: key };
    pending.push({
      key,
      label: def.label || key,
      hint: def.hint,
      url: def.url,
      url_label: def.url_label,
      secret: def.secret,
      required: def.required,
      targets: Array.from(paths).map(p => {
        const dot = p.indexOf('.');
        return dot < 0
          ? { connector: p, field_path: '' }
          : { connector: p.slice(0, dot), field_path: p.slice(dot + 1) };
      }),
    });
  }
  // Sort required first, then by label
  pending.sort((a, b) =>
    (b.required ? 1 : 0) - (a.required ? 1 : 0)
    || a.label.localeCompare(b.label),
  );

  return { pending, static_apply_targets: staticTargets, missing_manifests: missing, agents_preview: agentsPreview.length ? agentsPreview : undefined };
}

// ─── Apply ────────────────────────────────────────────────

async function apply(template: any, values: Record<string, string>): Promise<{
  applied: string[];
  installed_from_registry: string[];
  enabled_changed: string[];
  skipped_missing_manifest: string[];
  fields_preserved: Array<{ connector: string; field: string; reason: string }>;
  fields_left_empty: Array<{ connector: string; field: string }>;
  agents_applied?: string[];
}> {
  // Auto-inject user identity from settings so connectors (e.g. tp.username)
  // can use {user_name} / {user_email} without prompting the user again.
  // User-supplied values still win over auto-injected ones.
  const settings = loadSettings();
  values = {
    user_name: settings.displayName || '',
    user_email: settings.displayEmail || '',
    ...values,
  };

  // _derive — compute extra values from existing ones, so the template can
  // reference them like any other ${placeholder}. Each entry maps:
  //   "derived_key": "source_key"             → derive from values[source_key]
  //   "derived_key": { from: "x", op: "email_local" } → fancier
  //
  // Built-in op: 'email_local' = split @, take first half. Default op
  // (when value is a bare string) is also 'email_local' if the source
  // contains '@', otherwise a verbatim copy. Keeps template authors
  // honest — no full eval, just a couple of recipes.
  const deriveDict = (template?._derive ?? {}) as Record<string, unknown>;
  for (const [derivedKey, spec] of Object.entries(deriveDict)) {
    if (values[derivedKey]) continue; // user already provided → respect it
    let sourceKey: string;
    let op = 'auto';
    if (typeof spec === 'string') {
      sourceKey = spec;
    } else if (spec && typeof spec === 'object' && typeof (spec as any).from === 'string') {
      sourceKey = (spec as any).from;
      op = String((spec as any).op || 'auto');
    } else {
      continue;
    }
    const raw = values[sourceKey] ?? '';
    if (!raw) continue;
    let derived = raw;
    if (op === 'email_local' || (op === 'auto' && raw.includes('@'))) {
      derived = raw.split('@')[0];
    }
    values[derivedKey] = derived;
  }

  // Decrypt any `ent:…` blobs anywhere in the template up front. Enterprise
  // templates declare their key name via `_enterprise_name` (e.g. "fortinet");
  // anything below uses the plaintext as if it had been there all along.
  const enterpriseName = (template?._enterprise_name as string | undefined) || '';
  if (enterpriseName) {
    template = decryptDeep(template, enterpriseName);
  }

  const applied: string[] = [];
  const installedFromRegistry: string[] = [];
  const enabledChanged: string[] = [];
  const missing: string[] = [];
  const preserved: Array<{ connector: string; field: string; reason: string }> = [];
  const leftEmpty: Array<{ connector: string; field: string }> = [];

  for (const [id, row] of Object.entries(template)) {
    if (isMetaKey(id)) continue;
    let def = getConnector(id);
    if (!def) {
      // Manifest not installed yet — pull from forge-connectors registry
      // and install. installFromRegistry auto-syncs cache if empty.
      const r = await installFromRegistry(id);
      if (r.ok) {
        installedFromRegistry.push(id);
        def = getConnector(id);
      }
      if (!def) { missing.push(id); continue; }
    }

    const templateConfig = (row as any)?.config ?? {};
    const enabledTemplate = (row as any)?.enabled;

    const existing = getInstalledConnector(id)?.config ?? {};
    const merged: Record<string, unknown> = { ...existing };

    for (const [field, rawVal] of Object.entries(templateConfig)) {
      const resolved = substituteAll(rawVal, values);

      // If still contains unresolved ${...} (user left blank for non-required),
      // keep resolved as-is (empty strings collapsed already by substituteString).
      const existingVal = existing[field];
      const existingNonEmpty = !isEffectivelyEmpty(existingVal)
        && !(typeof existingVal === 'string' && existingVal.startsWith('TODO_'));

      if (existingNonEmpty) {
        // Compare — if template would have set the same thing (after substitution),
        // no-op silently; otherwise note as preserved.
        if (JSON.stringify(existingVal) !== JSON.stringify(resolved)) {
          preserved.push({ connector: id, field, reason: 'existing value kept' });
        }
        continue;
      }

      if (isEffectivelyEmpty(resolved)) {
        leftEmpty.push({ connector: id, field });
        // still write empty so the field shape exists (e.g. instances=[] is valid)
        // — but for plain string fields, an empty string is fine too.
      }
      merged[field] = resolved;
    }

    setConnectorConfig(id, merged);
    applied.push(id);

    if (typeof enabledTemplate === 'boolean') {
      setConnectorEnabled(id, enabledTemplate);
      enabledChanged.push(id);
    }
  }

  // ─── _agents block — pre-baked CLI agent presets ───────────
  //
  // Enterprise templates can declare claude-code / codex / aider
  // agent entries (with env vars, custom flags) that get merged into
  // `settings.agents`. Existing entries with the same id are kept
  // unless the template forces an overwrite by setting `_overwrite: true`
  // on that agent — by default we preserve user customisation.
  const agentsApplied: string[] = [];
  const agentsDict = template._agents as Record<string, any> | undefined;
  if (agentsDict && typeof agentsDict === 'object') {
    const freshSettings = loadSettings();
    const currentAgents = { ...(freshSettings.agents || {}) };
    let changed = false;
    for (const [agentId, raw] of Object.entries(agentsDict)) {
      if (!raw || typeof raw !== 'object') continue;
      const existing = currentAgents[agentId];
      const overwrite = (raw as any)._overwrite === true;
      if (existing && !overwrite) continue; // preserve user's tweaks
      const { _overwrite, ...entry } = raw as Record<string, unknown>;
      currentAgents[agentId] = entry as AgentEntry;
      agentsApplied.push(agentId);
      changed = true;
    }
    if (changed) {
      saveSettings({ ...freshSettings, agents: currentAgents });
    }
  }

  return {
    applied,
    installed_from_registry: installedFromRegistry,
    enabled_changed: enabledChanged,
    skipped_missing_manifest: missing,
    fields_preserved: preserved,
    fields_left_empty: leftEmpty,
    agents_applied: agentsApplied,
  };
}

// ─── Route ────────────────────────────────────────────────

/**
 * HEAD — existence probe. Always 200 because Forge ships a bundled
 * template. Kept HEAD-only for the UI mount probe (no body cost).
 */
export async function HEAD() {
  return new NextResponse(null, { status: 200 });
}

/**
 * GET — one-click flow. Returns an analyzed template:
 *   1. user override at <dataDir>/config-template.json, OR
 *   2. bundled fallback (templates/connector-config-template.json).
 * The UI uses this to pop the prompt modal without a file picker.
 */
export async function GET() {
  try {
    const { source, template, path } = resolveTemplate();
    const result = analyze(template);
    return NextResponse.json({ ok: true, source, path, template, ...result });
  } catch (e) {
    return NextResponse.json(
      { ok: false, error: e instanceof Error ? e.message : String(e) },
      { status: 500 },
    );
  }
}

export async function POST(req: Request) {
  const ct = req.headers.get('content-type') || '';

  try {
    // Phase 1: analyze (multipart upload)
    if (ct.startsWith('multipart/form-data')) {
      const fd = await req.formData();
      const file = fd.get('file');
      if (!(file instanceof File)) {
        return NextResponse.json({ ok: false, error: 'file field required' }, { status: 400 });
      }
      const text = await file.text();
      let template: any;
      try { template = JSON.parse(text); }
      catch (e) {
        return NextResponse.json(
          { ok: false, error: `invalid JSON: ${(e as Error).message}` },
          { status: 400 },
        );
      }
      const result = analyze(template);
      return NextResponse.json({ ok: true, template, ...result });
    }

    // Phase 2: apply (JSON body)
    if (ct.includes('application/json')) {
      const body = await req.json();
      const template = body?.template;
      const values = (body?.values ?? {}) as Record<string, string>;
      if (!template || typeof template !== 'object') {
        return NextResponse.json({ ok: false, error: 'template required' }, { status: 400 });
      }
      const result = await apply(template, values);
      return NextResponse.json({ ok: true, ...result });
    }

    return NextResponse.json(
      { ok: false, error: 'send multipart with `file` (analyze) or JSON {template,values} (apply)' },
      { status: 400 },
    );
  } catch (e) {
    return NextResponse.json(
      { ok: false, error: e instanceof Error ? e.message : String(e) },
      { status: 500 },
    );
  }
}
