// Unified marketplace domain layer.
//
// Bridges two backing stores:
//   1. `tool_installations` (managed by lib/tools/registry.js) — source of truth
//      for `tool` kind items. Every row in MARKETPLACE_TOOLS is exposed here as
//      a kind:'tool' MarketplaceItem with risk/approval/connector metadata.
//   2. `marketplace_items` + `marketplace_installations` — store for non-tool
//      kinds (skill, bundle, and any future kinds). Tool-kind items are NEVER
//      written here — that would create two writeable rows per tool.
//
// Phase 1 surfaces kinds: 'tool' | 'skill' | 'bundle'. Requesting 'agent' (the
// future Agent Marketplace) returns an empty array — that product has its own
// catalog and route and is intentionally out of scope here.

import fs from 'node:fs';
import path from 'node:path';
import { getDb, nowIso } from './db.js';
import { WORKSPACE } from './paths.js';
import { isInsideAllowedRoots } from './tools/safety.js';
import {
  listMarketplaceTools,
  setToolEnabled,
} from './tools/registry.js';
import { installAgentTemplate, uninstallAgent } from './agent-templates.js';
import {
  getDefinition,
  upsertDefinition,
  archiveDefinition,
} from './process/definitions.js';
import { installModule, uninstallModule } from './marketplace-modules.js';

const SUPPORTED_KINDS = new Set(['tool', 'skill', 'bundle', 'agent', 'connection', 'workflow', 'module']);
const PHASE1_DEFERRED_KINDS = new Set([]);

function safeParse(json, fallback) {
  if (!json) return fallback;
  try {
    const parsed = JSON.parse(json);
    return parsed == null ? fallback : parsed;
  } catch {
    return fallback;
  }
}

function shapeToolItem(tool) {
  const enabled = Boolean(tool.enabled);
  const available = Boolean(tool.available);
  return {
    id: `tool:${tool.name}`,
    kind: 'tool',
    slug: tool.name,
    title: tool.title || tool.name,
    description: tool.description || '',
    category: tool.category,
    author: 'madarx',
    source: { kind: 'builtin', url: null },
    version: null,
    installed: enabled,
    available,
    risk_summary: null,
    tags: [tool.category, tool.risk_level].filter(Boolean),
    tool: {
      risk_level: tool.risk_level,
      approval_mode: tool.approval_mode,
      required_env: tool.required_env || [],
      missing_env: tool.missing_env || [],
      available,
      connector: tool.connector || null,
    },
    skill: null,
    bundle: null,
    agent: null,
  };
}

function shapeNonToolItem(row, installRow) {
  const manifest = safeParse(row.manifest_json, {});
  const tags = safeParse(row.tags, []);
  const settings = safeParse(installRow?.settings_json, {}) || {};
  const item = {
    id: row.id,
    kind: row.kind,
    slug: row.slug,
    title: row.title,
    description: row.description || '',
    category: row.category,
    author: row.author,
    source: { kind: row.source_kind, url: row.source_url || null },
    version: row.version || null,
    installed: Boolean(installRow?.installed),
    available: true,
    risk_summary: row.risk_summary || null,
    tags: Array.isArray(tags) ? tags : [],
    tool: null,
    skill: null,
    bundle: null,
    agent: null,
    connection: null,
    workflow: null,
    module: null,
  };

  if (row.kind === 'skill') {
    item.skill = {
      invocation: manifest.invocation || `/${row.slug}`,
      file_paths: Array.isArray(manifest.file_paths) ? manifest.file_paths : [],
      trigger_examples: Array.isArray(manifest.trigger_examples)
        ? manifest.trigger_examples
        : [],
    };
  } else if (row.kind === 'connection') {
    item.connection = {
      service_key: manifest.service_key || row.slug,
      setup_kind: manifest.setup_kind || 'api_key',
      benefit: manifest.benefit || row.description || '',
      credentials: Array.isArray(manifest.credentials) ? manifest.credentials : [],
      verify_endpoint: manifest.verify_endpoint || null,
      verify_supported: Boolean(manifest.verify_supported),
      deep_link: manifest.deep_link || null,
      unlocks_tool_slugs: Array.isArray(manifest.unlocks_tool_slugs)
        ? manifest.unlocks_tool_slugs
        : [],
      engines: Array.isArray(manifest.engines) ? manifest.engines : [],
      logo_glyph: manifest.logo_glyph || row.slug,
      requires_daemon: manifest.requires_daemon || null,
      last_verified_at: settings.last_verified_at || null,
      account_label: settings.account_label || null,
      last_4: settings.last_4 || null,
      summary: settings.summary || null,
    };
  } else if (row.kind === 'agent') {
    item.agent = manifest.agent || manifest || {};
    try {
      const db = getDb();
      const installs = db.prepare("SELECT agent_id, project_id FROM agent_template_installations WHERE template_slug = ? AND status = 'active'").all(row.slug);
      item.installed = installs.length > 0;
      item.install_count = installs.length;
      item.agent.installed_instances = installs.map((install) => ({
        agent_id: install.agent_id,
        project_id: install.project_id,
      }));
    } catch {}
  } else if (row.kind === 'bundle') {
    item.bundle = {
      item_refs: Array.isArray(manifest.item_refs) ? manifest.item_refs : [],
      published_count: Number.isFinite(manifest.published_count)
        ? manifest.published_count
        : (Array.isArray(manifest.item_refs) ? manifest.item_refs.length : 0),
      license: manifest.license || null,
      note: manifest.note || null,
    };
  } else if (row.kind === 'workflow') {
    // Workflow templates ship a `source_spec_json` payload — the same shape
    // upsertDefinition() validates. Installing writes the spec into
    // process_definitions; uninstalling soft-archives it. The manifest also
    // surfaces stage count + first-stage form fields for the card without
    // requiring the client to parse the full JSON.
    const spec = manifest.source_spec_json && typeof manifest.source_spec_json === 'object'
      ? manifest.source_spec_json
      : null;
    const stageCount = spec && Array.isArray(spec.stages) ? spec.stages.length : 0;
    const stageKinds = spec && Array.isArray(spec.stages)
      ? spec.stages.map((s) => s && s.kind).filter(Boolean)
      : [];
    item.workflow = {
      definition_slug: spec?.slug || row.slug,
      schema_version: spec?.schema_version || 1,
      stage_count: stageCount,
      stage_kinds: stageKinds,
      summary: manifest.summary || row.description || '',
      // Full spec is attached so setItemInstalled can re-validate and write
      // through upsertDefinition without a second DB read. Marketplace items
      // are capped at 64 KB by the validator. For very long lists this is the
      // budget — if it ever becomes a problem, switch to omit-by-default and
      // add a `?include_spec=true` opt-in.
      source_spec_json: spec,
    };
    // Installed state for workflows is ownership-aware. A same-slug workflow
    // authored by a user is not considered this marketplace item's install.
    try {
      const ownedSlug = settings.workflow_definition_slug;
      const ownedItemId = settings.installed_by_marketplace_item_id;
      if (installRow?.installed && ownedItemId === row.id && ownedSlug) {
        const existing = getDefinition(ownedSlug);
        item.installed = Boolean(existing && existing.status !== 'archived');
      }
    } catch {
      // best effort — manifest may be malformed; keep installed=false
    }
  } else if (row.kind === 'module') {
    // Module manifests carry the module_id, agent list, tool list, skill packs,
    // and workflow templates. Surfaces metadata for the module card.
    item.module = {
      module_id: manifest.module_id || row.slug,
      agents: Array.isArray(manifest.agents) ? manifest.agents : [],
      tools: Array.isArray(manifest.tools) ? manifest.tools : [],
      skill_packs: Array.isArray(manifest.skill_packs) ? manifest.skill_packs : [],
      connections: Array.isArray(manifest.connections) ? manifest.connections : [],
      workflow_templates: Array.isArray(manifest.workflow_templates)
        ? manifest.workflow_templates
        : [],
      // Sidebar surfaces this module adds when installed: { label, href, icon, group }.
      nav: Array.isArray(manifest.nav) ? manifest.nav : [],
    };
  }

  return item;
}

function listToolItems() {
  return listMarketplaceTools().map(shapeToolItem);
}

function listNonToolItems(kindFilter) {
  const db = getDb();
  const params = [];
  let sql = `
    SELECT i.*, m.installed, m.settings_json
      FROM marketplace_items i
 LEFT JOIN marketplace_installations m ON m.item_id = i.id
  `;
  if (kindFilter) {
    sql += ' WHERE i.kind = ?';
    params.push(kindFilter);
  }
  sql += ' ORDER BY i.kind ASC, i.created_at ASC';
  const rows = db.prepare(sql).all(...params);
  return rows.map(row => shapeNonToolItem(row, {
    installed: row.installed,
    settings_json: row.settings_json,
  }));
}

function applyCategoryFilter(items, category) {
  if (!category) return items;
  const needle = String(category).toLowerCase();
  return items.filter(item => String(item.category || '').toLowerCase() === needle);
}

function searchScore(item, q) {
  if (!q) return 0;
  const needle = q.toLowerCase();
  const title = String(item.title || '').toLowerCase();
  const slug = String(item.slug || '').toLowerCase();
  const description = String(item.description || '').toLowerCase();
  const author = String(item.author || '').toLowerCase();
  const tags = Array.isArray(item.tags) ? item.tags.join(' ').toLowerCase() : '';

  if (title.startsWith(needle) || slug.startsWith(needle)) return 100;
  if (title.includes(needle) || slug.includes(needle)) return 80;
  if (author.includes(needle)) return 60;
  if (tags.includes(needle)) return 50;
  if (description.includes(needle)) return 30;
  return 0;
}

function applySearch(items, query) {
  if (!query) return items;
  const q = String(query).trim();
  if (!q) return items;
  return items
    .map(item => ({ item, score: searchScore(item, q) }))
    .filter(entry => entry.score > 0)
    .sort((a, b) => b.score - a.score)
    .map(entry => entry.item);
}

/**
 * List marketplace items, optionally filtered by kind, category, and a free-form
 * search query. Always returns the unified MarketplaceItem shape.
 *
 * @param {{ kind?: string, category?: string, query?: string }} opts
 */
export function listMarketplaceItems(opts = {}) {
  const { kind, category, query } = opts;

  if (kind && !SUPPORTED_KINDS.has(kind)) {
    return [];
  }

  let items;
  if (kind === 'tool') {
    items = listToolItems();
  } else if (
    kind === 'skill' ||
    kind === 'bundle' ||
    kind === 'agent' ||
    kind === 'connection' ||
    kind === 'workflow' ||
    kind === 'module'
  ) {
    items = listNonToolItems(kind);
  } else {
    items = [...listToolItems(), ...listNonToolItems()];
  }

  items = applyCategoryFilter(items, category);
  items = applySearch(items, query);
  return items;
}

/**
 * Return a single item by id, or null if not found.
 * Tool-kind ids are `tool:<slug>`; skill/bundle ids match the marketplace_items.id column.
 */
export function getItem(id) {
  if (!id) return null;
  if (typeof id === 'string' && id.startsWith('tool:')) {
    const slug = id.slice('tool:'.length);
    const tool = listMarketplaceTools().find(t => t.name === slug);
    return tool ? shapeToolItem(tool) : null;
  }
  const db = getDb();
  const row = db.prepare(`
    SELECT i.*, m.installed, m.settings_json
      FROM marketplace_items i
 LEFT JOIN marketplace_installations m ON m.item_id = i.id
     WHERE i.id = ?
  `).get(id);
  return row ? shapeNonToolItem(row, {
    installed: row.installed,
    settings_json: row.settings_json,
  }) : null;
}

/**
 * Toggle the install state of a marketplace item. Dispatches on kind:
 *   - tool   → proxies to setToolEnabled (writes tool_installations)
 *   - skill  → writes marketplace_installations
 *   - bundle → returns { status: 'deferred' } (Phase 2 will perform the install)
 *
 * Returns the updated item, or a structured deferred status for bundles.
 */
export function setItemInstalled(id, installed) {
  const item = getItem(id);
  if (!item) throw new Error(`Unknown marketplace item: ${id}`);

  if (item.kind === 'tool') {
    setToolEnabled(item.slug, Boolean(installed));
    return getItem(id);
  }

  if (item.kind === 'agent') {
    if (installed) installAgentTemplate(item.slug, 'personal');
    else {
      const instances = item.agent?.installed_instances || [];
      if (instances[0]?.agent_id) uninstallAgent(instances[0].agent_id);
    }
    return getItem(id);
  }

  if (item.kind === 'bundle') {
    return {
      status: 'deferred',
      reason: 'Bundle install lands in Phase 2 (GitHub importer).',
      item,
    };
  }

  if (item.kind === 'connection') {
    return {
      status: 'setup_required',
      reason: 'Open the setup assistant to connect this service.',
      item,
    };
  }

  if (item.kind === 'workflow') {
    return setWorkflowItemInstalled(item, installed);
  }

  if (item.kind === 'module') {
    return installed ? installModule(item, getItem) : uninstallModule(item, getItem);
  }

  if (item.kind === 'skill' && installed) {
    // Phase-2 skill file install: materialize the SKILL.md into the workspace so
    // the skill is actually usable, not just flagged installed. Best-effort —
    // a seeded skill that ships its file (no source_markdown) is a no-op.
    materializeSkillFiles(item);
  }

  // skill (and any future non-tool kinds) — write through marketplace_installations.
  const db = getDb();
  const now = nowIso();
  const existing = db
    .prepare('SELECT item_id FROM marketplace_installations WHERE item_id=?')
    .get(id);

  if (existing) {
    db.prepare(`
      UPDATE marketplace_installations
         SET installed=?, installed_at=?
       WHERE item_id=?
    `).run(installed ? 1 : 0, installed ? now : null, id);
  } else {
    db.prepare(`
      INSERT INTO marketplace_installations (item_id, installed, installed_at)
      VALUES (?, ?, ?)
    `).run(id, installed ? 1 : 0, installed ? now : null);
  }

  return getItem(id);
}

/**
 * Install or uninstall a marketplace workflow template.
 *
 * Install: validates the source spec and writes a process_definitions row via
 * upsertDefinition (the SOLE writer for that table). If a definition at the
 * spec's slug already exists, returns { status: 'already_present' } — we never
 * silently overwrite user-edited workflows.
 *
 * Uninstall: soft-archives the definition. archiveDefinition refuses to archive
 * if any non-terminal cases are still running (FK-safe), so the install record
 * stays "installed" until those cases finish. The marketplace_installations row
 * is only flipped after the archive succeeds.
 */
function setWorkflowItemInstalled(item, installed) {
  const spec = item?.workflow?.source_spec_json;
  if (!spec || typeof spec !== 'object') {
    return {
      status: 'error',
      reason: 'Workflow template has no source spec',
      item,
    };
  }

  const slug = spec.slug || item.workflow.definition_slug;
  if (!slug) {
    return { status: 'error', reason: 'Workflow spec missing slug', item };
  }

  const db = getDb();
  const now = nowIso();

  if (installed) {
    const existing = getDefinition(slug);
    if (existing && existing.status !== 'archived') {
      return {
        status: 'already_present',
        reason: 'A workflow with this slug already exists. It was not installed as this marketplace item, so uninstall will not archive it.',
        item: getItem(item.id),
      };
    }
    try {
      upsertDefinition(spec, {
        source_spec_json: JSON.stringify(spec),
        created_by_user_id: null,
      });
      installRequiredWorkflowAgentTemplates(spec, 'personal');
    } catch (err) {
      return {
        status: 'error',
        reason: err?.code === 'spec_invalid'
          ? `Workflow spec invalid: ${(err.errors || []).join('; ')}`
          : (err?.message || 'Workflow install failed'),
        item,
      };
    }
    writeInstallRow(db, item.id, true, now, {
      installed_by_marketplace_item_id: item.id,
      workflow_definition_slug: slug,
      workflow_definition_id: `procdef:${slug}`,
      installed_source: 'marketplace',
    });
    return getItem(item.id);
  }

  // Uninstall path: only archive definitions that this marketplace item
  // actually installed. Same-slug user-authored workflows are left alone.
  const installRow = db.prepare('SELECT settings_json FROM marketplace_installations WHERE item_id=? AND installed=1').get(item.id);
  const settings = safeParse(installRow?.settings_json, {}) || {};
  const ownsDefinition = settings.installed_by_marketplace_item_id === item.id
    && settings.workflow_definition_slug === slug;
  if (!ownsDefinition) {
    writeInstallRow(db, item.id, false, now, settings);
    return getItem(item.id);
  }

  // Soft-archive the owned definition. FK-safe — archiveDefinition refuses if
  // non-terminal cases reference this definition.
  const result = archiveDefinition(slug);
  if (!result.ok && result.error === 'in_flight_cases') {
    return {
      status: 'in_flight_cases',
      in_flight_cases: result.in_flight_cases,
      reason: `Cannot uninstall — ${result.in_flight_cases} case(s) still running. Finish or cancel them first.`,
      item: getItem(item.id),
    };
  }
  // Either archived successfully or no definition existed (no-op).
  writeInstallRow(db, item.id, false, now);
  return getItem(item.id);
}

/**
 * Materialize an authored skill's SKILL.md into the workspace on install.
 * The full markdown is carried in manifest.source_markdown (tool-smith authored
 * skills). Routes the write through resolveSafePath so files can only land under
 * the workspace allowed root. Best-effort and non-fatal.
 */
function materializeSkillFiles(item) {
  try {
    const db = getDb();
    const row = db.prepare("SELECT manifest_json FROM marketplace_items WHERE id=?").get(item.id);
    const manifest = safeParse(row?.manifest_json, {});
    const markdown = typeof manifest.source_markdown === 'string' ? manifest.source_markdown : null;
    if (!markdown) return; // seeded/vendored skill — file already on disk.
    const filePaths = Array.isArray(manifest.file_paths) && manifest.file_paths.length
      ? manifest.file_paths
      : [`skills/${item.slug}/SKILL.md`];
    const rel = filePaths[0];
    const target = path.join(WORKSPACE, rel);
    if (!isInsideAllowedRoots(target)) return; // escape attempt — skip silently.
    fs.mkdirSync(path.dirname(target), { recursive: true });
    if (!fs.existsSync(target)) fs.writeFileSync(target, markdown, 'utf8');
  } catch {
    // Non-fatal — install flag still flips; the file can be re-authored.
  }
}

function writeInstallRow(db, itemId, installed, now, settings = null) {
  const existing = db
    .prepare('SELECT item_id FROM marketplace_installations WHERE item_id=?')
    .get(itemId);
  const settingsJson = settings ? JSON.stringify(settings) : null;
  if (existing) {
    db.prepare(`
      UPDATE marketplace_installations
         SET installed=?, installed_at=?, settings_json=?
       WHERE item_id=?
    `).run(installed ? 1 : 0, installed ? now : null, settingsJson, itemId);
  } else {
    db.prepare(`
      INSERT INTO marketplace_installations (item_id, installed, installed_at, settings_json)
      VALUES (?, ?, ?, ?)
    `).run(itemId, installed ? 1 : 0, installed ? now : null, settingsJson);
  }
}

function workflowAgentTemplateSlugs(spec) {
  if (!spec || !Array.isArray(spec.stages)) return [];
  const slugs = new Set();
  for (const stage of spec.stages) {
    if (stage?.kind !== 'agent_run') continue;
    const slug = stage?.config?.agent_template_slug;
    if (typeof slug === 'string' && slug.trim()) slugs.add(slug.trim());
  }
  return [...slugs];
}

function installRequiredWorkflowAgentTemplates(spec, projectId = 'personal') {
  for (const slug of workflowAgentTemplateSlugs(spec)) {
    installAgentTemplate(slug, projectId);
  }
}

/**
 * Free-form search across every kind. Convenience wrapper around
 * listMarketplaceItems({ query }).
 */
export function searchItems(query) {
  return listMarketplaceItems({ query });
}

/**
 * Flattened sidebar nav contributed by every INSTALLED module. The sidebar
 * renders these alongside the static core items, so installing a module adds
 * its surface(s) and uninstalling removes them.
 *
 * @returns {Array<{ label, href, icon, group }>}
 */
export function getInstalledModuleNav() {
  const out = [];
  try {
    for (const item of listMarketplaceItems({ kind: 'module' })) {
      if (!item.installed) continue;
      for (const n of item.module?.nav || []) {
        if (n && typeof n.href === 'string' && typeof n.label === 'string') {
          out.push({
            label: n.label,
            href: n.href,
            icon: typeof n.icon === 'string' ? n.icon : 'Package',
            group: typeof n.group === 'string' ? n.group : 'Modules',
          });
        }
      }
    }
  } catch {
    // Sidebar must render even if the marketplace read fails.
  }
  return out;
}

/**
 * Counts grouped by kind. Useful for the kind tabs in the marketplace UI.
 */
export function getKindCounts() {
  const tools = listToolItems().length;
  const nonTool = listNonToolItems();
  const skills = nonTool.filter(item => item.kind === 'skill').length;
  const bundles = nonTool.filter(item => item.kind === 'bundle').length;
  const agents = nonTool.filter(item => item.kind === 'agent').length;
  const connections = nonTool.filter(item => item.kind === 'connection').length;
  const workflows = nonTool.filter(item => item.kind === 'workflow').length;
  const modules = nonTool.filter(item => item.kind === 'module').length;
  return {
    tool: tools,
    skill: skills,
    bundle: bundles,
    agent: agents,
    connection: connections,
    workflow: workflows,
    module: modules,
    all: tools + skills + bundles + agents + connections + workflows + modules,
  };
}

/**
 * Unique category list per kind, for the category-chip filter row.
 */
export function listCategoriesForKind(kind) {
  const items = listMarketplaceItems({ kind });
  const seen = new Set();
  const categories = [];
  for (const item of items) {
    if (!item.category || seen.has(item.category)) continue;
    seen.add(item.category);
    categories.push(item.category);
  }
  return categories;
}

export { SUPPORTED_KINDS, PHASE1_DEFERRED_KINDS };
