/**
 * Workflow Marketplace — sync recipes + pipelines from a Git repo.
 *
 * Repo layout (default: aiwatching/forge-workflow):
 *
 *   registry.json
 *     { version, updated_at, recipes: [...summary], pipelines: [...summary] }
 *   recipes/<name>/recipe.yaml   ← the actual recipe content
 *   recipes/<name>/info.json     ← optional richer metadata (rating, tags)
 *   pipelines/<name>/pipeline.yaml
 *   pipelines/<name>/info.json
 *
 * Local cache: <dataDir>/workflow-cache.json — last fetched registry.
 * Installed status is derived by checking the files on disk under
 *   <dataDir>/recipes/  (handled by lib/jobs/recipes.ts)
 *   <dataDir>/flows/    (handled by lib/pipeline.ts)
 */

import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import YAML from 'yaml';
import { getDataDir } from './dirs';
import { loadSettings } from './settings';
import { listRecipes } from './jobs/recipes';
import { listWorkflows } from './pipeline';

export type WorkflowKind = 'recipe' | 'pipeline';

export interface MarketplaceEntry {
  kind: WorkflowKind;
  name: string;
  display_name: string;
  description?: string;
  version: string;
  author?: string;
  tags?: string[];
  score?: number;
  rating?: number;
  source: 'registry' | 'local';
  installed: boolean;
  installed_version?: string;
  has_update?: boolean;
}

const CACHE_FILE = () => join(getDataDir(), 'workflow-cache.json');
const REQUEST_TIMEOUT_MS = 10_000;

function baseUrl(): string {
  return (loadSettings().workflowRepoUrl || '').replace(/\/+$/, '');
}

interface RegistryFile {
  version: number;
  updated_at?: string;
  recipes?: Array<Partial<MarketplaceEntry>>;
  pipelines?: Array<Partial<MarketplaceEntry>>;
}

function readCache(): RegistryFile | null {
  try {
    const path = CACHE_FILE();
    if (!existsSync(path)) return null;
    return JSON.parse(readFileSync(path, 'utf-8'));
  } catch { return null; }
}

function writeCache(data: RegistryFile): void {
  mkdirSync(getDataDir(), { recursive: true });
  writeFileSync(CACHE_FILE(), JSON.stringify(data, null, 2), { mode: 0o600 });
}

// ─── Sync ──────────────────────────────────────────────────

export async function syncMarketplace(): Promise<{ ok: boolean; recipes: number; pipelines: number; error?: string }> {
  const base = baseUrl();
  if (!base) return { ok: false, recipes: 0, pipelines: 0, error: 'workflowRepoUrl is empty' };
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
  try {
    const cacheBust = `_t=${Date.now()}`;
    const res = await fetch(`${base}/registry.json?${cacheBust}`, {
      signal: controller.signal,
      headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' },
    });
    clearTimeout(timer);
    if (!res.ok) return { ok: false, recipes: 0, pipelines: 0, error: `registry fetch failed: HTTP ${res.status}` };
    const data = (await res.json()) as RegistryFile;
    writeCache(data);
    console.log(`[workflow-marketplace] synced ${data.recipes?.length ?? 0} recipes + ${data.pipelines?.length ?? 0} pipelines`);
    return { ok: true, recipes: data.recipes?.length ?? 0, pipelines: data.pipelines?.length ?? 0 };
  } catch (e) {
    clearTimeout(timer);
    const msg = (e as Error).message || String(e);
    console.warn(`[workflow-marketplace] sync failed:`, msg);
    return { ok: false, recipes: 0, pipelines: 0, error: msg };
  }
}

// ─── List (merged registry + installed) ────────────────────

function compareVersions(a: string, b: string): number {
  const pa = (a || '0.0.0').split('.').map(Number);
  const pb = (b || '0.0.0').split('.').map(Number);
  for (let i = 0; i < 3; i++) {
    const diff = (pa[i] || 0) - (pb[i] || 0);
    if (diff !== 0) return diff;
  }
  return 0;
}

export function listMarketplace(): { recipes: MarketplaceEntry[]; pipelines: MarketplaceEntry[] } {
  const cache: RegistryFile = readCache() || { version: 1, recipes: [], pipelines: [] };
  const installedRecipes = listRecipes();
  const installedWorkflows = listWorkflows();

  const recipeMap = new Map<string, MarketplaceEntry>();
  for (const r of (cache.recipes || [])) {
    if (!r.name) continue;
    recipeMap.set(r.name, {
      kind: 'recipe',
      name: r.name,
      display_name: r.display_name || r.name,
      description: r.description,
      version: r.version || '0.0.0',
      author: r.author,
      tags: r.tags,
      score: r.score ?? 0,
      rating: r.rating ?? 0,
      source: 'registry',
      installed: false,
    });
  }
  for (const ir of installedRecipes) {
    const cached = recipeMap.get(ir.name);
    if (cached) {
      cached.installed = true;
      cached.installed_version = ir.version;
      cached.has_update = compareVersions(cached.version, ir.version) > 0;
    } else {
      // Local-only recipe (uploaded, not in marketplace)
      recipeMap.set(ir.name, {
        kind: 'recipe',
        name: ir.name,
        display_name: ir.display_name,
        description: ir.description,
        version: ir.version,
        author: ir.author,
        tags: ir.tags,
        source: 'local',
        installed: true,
        installed_version: ir.version,
      });
    }
  }

  const pipelineMap = new Map<string, MarketplaceEntry>();
  for (const p of (cache.pipelines || [])) {
    if (!p.name) continue;
    pipelineMap.set(p.name, {
      kind: 'pipeline',
      name: p.name,
      display_name: p.display_name || p.name,
      description: p.description,
      version: p.version || '0.0.0',
      author: p.author,
      tags: p.tags,
      score: p.score ?? 0,
      rating: p.rating ?? 0,
      source: 'registry',
      installed: false,
    });
  }
  for (const iw of installedWorkflows) {
    if (iw.builtin) continue;                  // built-ins aren't user workflows
    const cached = pipelineMap.get(iw.name);
    if (cached) {
      cached.installed = true;
      // pipeline yaml doesn't carry an explicit version; treat unknown.
      cached.installed_version = (iw as any).version || cached.version;
    } else {
      pipelineMap.set(iw.name, {
        kind: 'pipeline',
        name: iw.name,
        display_name: iw.name,
        description: iw.description,
        version: '0.0.0',
        source: 'local',
        installed: true,
      });
    }
  }

  return {
    recipes: Array.from(recipeMap.values()).sort((a, b) => a.display_name.localeCompare(b.display_name)),
    pipelines: Array.from(pipelineMap.values()).sort((a, b) => a.display_name.localeCompare(b.display_name)),
  };
}

// ─── Install ───────────────────────────────────────────────

async function fetchText(url: string): Promise<string | null> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
  try {
    const res = await fetch(url, { signal: controller.signal });
    clearTimeout(timer);
    if (!res.ok) return null;
    return await res.text();
  } catch { clearTimeout(timer); return null; }
}

export async function installFromMarketplace(
  kind: WorkflowKind,
  name: string,
  opts: { target_name?: string; overwrite?: boolean } = {},
): Promise<{ ok: boolean; error?: string; installed_as?: string }> {
  const base = baseUrl();
  if (!base) return { ok: false, error: 'workflowRepoUrl is empty' };
  if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) return { ok: false, error: 'invalid name' };

  const targetName = opts.target_name || name;
  if (!/^[a-z0-9][a-z0-9_-]*$/.test(targetName)) {
    return { ok: false, error: `target_name "${targetName}" must be lowercase alphanumerics + hyphens/underscores` };
  }

  // Recipes live under recipes/<name>/recipe.yaml, pipelines under
  // pipelines/<name>/pipeline.yaml. Falls back to a flat file too,
  // for repos that don't use a per-item folder.
  const subdir = kind === 'recipe' ? 'recipes' : 'pipelines';
  const file = kind === 'recipe' ? 'recipe.yaml' : 'pipeline.yaml';
  const candidates = [
    `${base}/${subdir}/${name}/${file}`,
    `${base}/${subdir}/${name}.yaml`,
  ];

  let yamlText: string | null = null;
  for (const url of candidates) {
    yamlText = await fetchText(url);
    if (yamlText) break;
  }
  if (!yamlText) return { ok: false, error: `${kind} "${name}" not found in registry` };

  // Validate it parses + has the right shape before writing.
  let parsed: any;
  try {
    parsed = YAML.parse(yamlText);
    if (!parsed?.name) return { ok: false, error: 'yaml missing top-level "name"' };
    if (parsed.name !== name) return { ok: false, error: `yaml name "${parsed.name}" doesn't match registry id "${name}"` };
    if (kind === 'recipe') {
      if (!Array.isArray(parsed.params)) return { ok: false, error: 'recipe yaml missing "params" array' };
      if (!parsed.job) return { ok: false, error: 'recipe yaml missing "job" block' };
    }
  } catch (e) {
    return { ok: false, error: `invalid YAML: ${(e as Error).message}` };
  }

  const targetDir = kind === 'recipe' ? join(getDataDir(), 'recipes') : join(getDataDir(), 'flows');
  mkdirSync(targetDir, { recursive: true, mode: 0o700 });
  const targetPath = join(targetDir, `${targetName}.yaml`);
  if (existsSync(targetPath) && !opts.overwrite) {
    return { ok: false, error: `"${targetName}" already exists locally — pass overwrite=true to replace it, or choose a different name` };
  }

  // If renaming, rewrite the yaml's top-level `name:` so the on-disk
  // copy matches its filename. Without this the file would be loaded
  // as the original name and collide with other clones.
  let outputYaml = yamlText;
  if (targetName !== name) {
    parsed.name = targetName;
    outputYaml = YAML.stringify(parsed);
  }
  writeFileSync(targetPath, outputYaml, { mode: 0o600 });
  return { ok: true, installed_as: targetName };
}

// ─── Uninstall ─────────────────────────────────────────────

export function uninstallFromMarketplace(kind: WorkflowKind, name: string): { ok: boolean; error?: string } {
  if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) return { ok: false, error: 'invalid name' };
  const dir = kind === 'recipe' ? join(getDataDir(), 'recipes') : join(getDataDir(), 'flows');
  const yamlPath = join(dir, `${name}.yaml`);
  const ymlPath = join(dir, `${name}.yml`);
  const { unlinkSync } = require('node:fs');
  if (existsSync(yamlPath)) { unlinkSync(yamlPath); return { ok: true }; }
  if (existsSync(ymlPath)) { unlinkSync(ymlPath); return { ok: true }; }
  return { ok: false, error: 'not installed' };
}
