/**
 * POST /api/connectors/install-local
 *
 * Install a connector from a user-supplied manifest. Two flavours:
 *
 *   multipart/form-data with field `file`:
 *     - .yaml / .yml  → single-file manifest
 *     - .zip          → multi-file bundle (must contain manifest.yaml
 *                       at the root; may add README.md, icon.svg,
 *                       tools/*.js, etc.)
 *
 *   application/json body `{ yaml: "...", id?: "..." }`:
 *     - convenience path for callers that already have the YAML in
 *       memory (Forge Help AI, internal scripts). `id` is optional
 *       — if missing, parsed from the manifest.
 *
 * Behaviour:
 *   1. Validate the manifest has `id`, `name`, and at least `tools` or
 *      `connectors[]`.
 *   2. Write the manifest (and any zip siblings) to
 *      <dataDir>/connectors/<id>/.
 *   3. Register/upsert the row in connector-configs.json with the
 *      manifest's version as installed_version, preserving any prior
 *      user-supplied settings (so re-installing a tweaked version
 *      doesn't blow away a PAT).
 *
 * Marketplace UI distinguishes "local" connectors from "registry"
 * connectors by membership in the registry cache — no extra source
 * field on disk.
 */

import { NextResponse } from 'next/server';
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { join, normalize, sep } from 'node:path';
import AdmZip from 'adm-zip';
import YAML from 'yaml';
import { getDataDir } from '@/lib/dirs';
import { installConnector } from '@/lib/connectors/registry';

const MAX_ZIP_BYTES = 5 * 1024 * 1024;          // 5 MB
const MAX_FILE_BYTES = 1 * 1024 * 1024;         // 1 MB per file inside the zip
const MAX_FILES_IN_ZIP = 50;

interface ParsedManifest {
  id: string;
  name: string;
  version?: string;
}

function parseManifest(yaml: string): ParsedManifest {
  let def: any;
  try {
    def = YAML.parse(yaml);
  } catch (e) {
    throw new Error(`invalid YAML: ${(e as Error).message}`);
  }
  if (!def?.id || typeof def.id !== 'string') {
    throw new Error('manifest.id is required');
  }
  if (!/^[a-z0-9][a-z0-9_-]*$/.test(def.id)) {
    throw new Error(`manifest.id "${def.id}" must be lowercase alphanumerics + hyphens/underscores`);
  }
  if (!def.name || typeof def.name !== 'string') {
    throw new Error('manifest.name is required');
  }
  if (!def.tools && !(Array.isArray(def.connectors) && def.connectors.length)) {
    throw new Error('manifest must declare `tools` or `connectors[]`');
  }
  return { id: def.id, name: def.name, version: def.version };
}

function safeEntryPath(name: string): string | null {
  // Reject absolute paths, parent-dir escapes, leading slashes.
  if (!name || name.startsWith('/') || name.includes('..')) return null;
  const normalized = normalize(name).replace(/^[/\\]+/, '');
  if (normalized.split(sep).some((seg) => seg === '..' || seg === '')) return null;
  return normalized;
}

function ensureDir(p: string): void {
  if (!existsSync(p)) mkdirSync(p, { recursive: true, mode: 0o700 });
}

interface InstallResult {
  ok: boolean;
  id?: string;
  version?: string;
  files_written?: number;
  error?: string;
}

function installFromFiles(
  manifestYaml: string,
  extraFiles: Array<{ path: string; data: Buffer }>,
): InstallResult {
  let parsed: ParsedManifest;
  try { parsed = parseManifest(manifestYaml); }
  catch (e) { return { ok: false, error: (e as Error).message }; }

  const dir = join(getDataDir(), 'connectors', parsed.id);
  ensureDir(dir);

  // Wipe stale siblings from a prior install of the same id (but
  // preserve nothing — local install replaces the directory).
  // For safety, we only write inside `dir`; safeEntryPath gated it.

  writeFileSync(join(dir, 'manifest.yaml'), manifestYaml, { mode: 0o600 });
  let written = 1;
  for (const f of extraFiles) {
    const target = join(dir, f.path);
    ensureDir(join(target, '..'));
    writeFileSync(target, f.data, { mode: 0o600 });
    written += 1;
  }

  const ok = installConnector(parsed.id, manifestYaml);
  if (!ok) {
    return { ok: false, error: 'installConnector rejected the manifest (re-parse failed)' };
  }
  return {
    ok: true,
    id: parsed.id,
    version: parsed.version || '0.0.0',
    files_written: written,
  };
}

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

  // ── JSON path: { yaml: "...", id?: "..." } ─────────────────
  if (ct.includes('application/json')) {
    let body: any = {};
    try { body = await req.json(); } catch {}
    const yaml = typeof body?.yaml === 'string' ? body.yaml : '';
    if (!yaml) {
      return NextResponse.json({ ok: false, error: 'yaml field is required' }, { status: 400 });
    }
    const r = installFromFiles(yaml, []);
    return NextResponse.json(r, { status: r.ok ? 200 : 400 });
  }

  // ── multipart: file = .yaml | .yml | .zip ──────────────────
  if (ct.includes('multipart/form-data')) {
    let form: FormData;
    try { form = await req.formData(); }
    catch (e) {
      return NextResponse.json({ ok: false, error: 'invalid multipart body' }, { status: 400 });
    }
    const file = form.get('file');
    if (!(file instanceof File)) {
      return NextResponse.json({ ok: false, error: 'file field missing' }, { status: 400 });
    }
    if (file.size > MAX_ZIP_BYTES) {
      return NextResponse.json({ ok: false, error: `file too large (max ${MAX_ZIP_BYTES} bytes)` }, { status: 413 });
    }

    const name = file.name.toLowerCase();
    const buf = Buffer.from(await file.arrayBuffer());

    if (name.endsWith('.yaml') || name.endsWith('.yml')) {
      const r = installFromFiles(buf.toString('utf-8'), []);
      return NextResponse.json(r, { status: r.ok ? 200 : 400 });
    }

    if (name.endsWith('.zip')) {
      let zip: AdmZip;
      try { zip = new AdmZip(buf); }
      catch (e) {
        return NextResponse.json({ ok: false, error: `invalid zip: ${(e as Error).message}` }, { status: 400 });
      }
      const entries = zip.getEntries().filter((e) => !e.isDirectory);
      if (entries.length === 0) {
        return NextResponse.json({ ok: false, error: 'zip is empty' }, { status: 400 });
      }
      if (entries.length > MAX_FILES_IN_ZIP) {
        return NextResponse.json({ ok: false, error: `zip has too many files (max ${MAX_FILES_IN_ZIP})` }, { status: 400 });
      }

      // Locate manifest.yaml at the root.
      const manifestEntry = entries.find((e) => {
        const safe = safeEntryPath(e.entryName);
        return safe === 'manifest.yaml' || safe === 'manifest.yml';
      });
      if (!manifestEntry) {
        return NextResponse.json({ ok: false, error: 'zip must contain manifest.yaml at the root' }, { status: 400 });
      }
      const manifestYaml = manifestEntry.getData().toString('utf-8');

      const extras: Array<{ path: string; data: Buffer }> = [];
      for (const e of entries) {
        if (e === manifestEntry) continue;
        const safe = safeEntryPath(e.entryName);
        if (!safe) {
          return NextResponse.json({ ok: false, error: `unsafe path in zip: ${e.entryName}` }, { status: 400 });
        }
        const data = e.getData();
        if (data.length > MAX_FILE_BYTES) {
          return NextResponse.json({ ok: false, error: `entry too large: ${safe}` }, { status: 413 });
        }
        extras.push({ path: safe, data });
      }
      const r = installFromFiles(manifestYaml, extras);
      return NextResponse.json(r, { status: r.ok ? 200 : 400 });
    }

    return NextResponse.json({ ok: false, error: 'unsupported file extension (expected .yaml/.yml/.zip)' }, { status: 400 });
  }

  return NextResponse.json({ ok: false, error: 'expected JSON or multipart/form-data' }, { status: 400 });
}
