/**
 * POST /api/jobs/preview
 *
 * Dry-run a Job's source_tool with its source_input and return the first
 * parsed item. The Jobs editor uses this to auto-build input_template:
 * "given the bug Mantis just returned, what {{item.X}} expression maps
 * each pipeline input field?" — eliminating the friction where users
 * had to know in advance that get_bug returns `id` not `bug_id` etc.
 *
 * NO dispatch happens. NO dedup mutation. NO job row required (this works
 * before the Job is saved, that's the whole point).
 *
 * Request body:
 *   {
 *     source_connector: string,
 *     source_tool: string,
 *     source_input?: object,
 *     items_path?: string,        // empty / dotted path
 *     pipeline_inputs?: string[]  // optional: workflow's input field names —
 *                                 // if given, response includes a suggested
 *                                 // input_template with fuzzy-matched mappings
 *   }
 *
 * Response:
 *   {
 *     ok: true,
 *     sample_item: object,            // first item from the connector
 *     item_keys: string[],            // top-level keys on sample_item
 *     suggested_template?: Record<string,string>  // pipeline_input → "{{item.X}}"
 *   }
 *
 *   or { ok: false, error, ...debug } on failure
 */

import { NextResponse } from 'next/server';
import { dispatchTool } from '@/lib/chat/tool-dispatcher';

function pickPath(obj: unknown, path: string): unknown {
  if (!path) return obj;
  const parts = path.split('.');
  let cur: any = obj;
  for (const p of parts) {
    if (cur == null || typeof cur !== 'object') return undefined;
    cur = cur[p];
  }
  return cur;
}

/**
 * Guess the right items_path purely from the response shape, so the UI
 * can offer it even when the user left the field blank (or set it wrong).
 *   - top-level array            → ''                      (scheduler iterates directly)
 *   - object with exactly one array-valued key → that key
 *   - object with several array-valued keys    → prefer common names
 *   - top-level single object    → ''                      (scheduler wraps as 1-item)
 */
function suggestItemsPath(parsed: unknown): string | null {
  if (Array.isArray(parsed)) return '';
  if (parsed && typeof parsed === 'object') {
    const obj = parsed as Record<string, unknown>;
    const arrayKeys = Object.entries(obj).filter(([, v]) => Array.isArray(v)).map(([k]) => k);
    if (arrayKeys.length === 0) return '';
    // Only suggest a non-empty items_path when one of the keys looks
    // like a *list payload* name. Otherwise this is probably a detail
    // object (e.g. mantis.get_bug returns {id, summary, ..., history,
    // notes}) — `history` is an internal array, not the items list.
    // For detail responses, empty items_path → scheduler wraps the
    // single object as a 1-item list.
    const common = ['items', 'data', 'results', 'records', 'bugs', 'issues', 'merge_requests', 'mrs', 'projects', 'pipelines'];
    for (const name of common) if (arrayKeys.includes(name)) return name;
    return '';
  }
  return null;
}

/**
 * Pick the most-likely unique-identifier key from the sample item.
 * Order matches what users typically expect, with `iid` (GitLab project-scoped
 * issue/MR id) preferred over the global `id` when both exist.
 */
function suggestDedupField(itemKeys: string[]): string | null {
  // Prefer the globally-unique `id` over GitLab's project-scoped `iid`
  // so dedup remains safe if a job ever spans multiple projects.
  const priority = ['id', 'iid', 'key', 'uuid', 'global_id', 'guid', 'sha', 'commit_id'];
  for (const p of priority) if (itemKeys.includes(p)) return p;
  const trailingId = itemKeys.find((k) => /_id$/i.test(k));
  if (trailingId) return trailingId;
  return null;
}

/**
 * Best-effort match a workflow input name to a key on the sample item.
 * The mantis case (bug_id ← id, base_branch ← nothing) drove this list:
 *   - Exact match wins.
 *   - Strip _id / Id / ID suffixes both sides → check.
 *   - 'description' fuzz: desc, body, content all work.
 *   - 'summary' fuzz: title, name.
 * Returns the matching item-key or null. NEVER guesses across unrelated names.
 */
function suggestMapping(inputName: string, itemKeys: string[]): string | null {
  const lc = inputName.toLowerCase();
  const lcMap = new Map(itemKeys.map(k => [k.toLowerCase(), k]));

  // 1. Exact match.
  if (lcMap.has(lc)) return lcMap.get(lc)!;

  // 2. ID-shaped fields. The interesting cases:
  //    - pipeline input "bug_id" + item has "id"        → match "id"
  //    - pipeline input "bug_id" + item has "bug"       → match "bug"
  //    - pipeline input "bug_id" + item has "bugId"     → match "bugId"
  //    - pipeline input "id"     + item has "issue_id"  → match "issue_id"
  const endsWithId = /_?id$/i.test(lc);
  const stripped = lc.replace(/_?id$/i, '');
  if (endsWithId) {
    if (lcMap.has('id')) return lcMap.get('id')!;                  // ← the missing case
    if (stripped && lcMap.has(stripped)) return lcMap.get(stripped)!;
    if (stripped && lcMap.has(stripped + '_id')) return lcMap.get(stripped + '_id')!;
  } else if (lc === 'id') {
    for (const k of itemKeys) {
      if (/_?id$/i.test(k.toLowerCase())) return k;
    }
  }

  // 3. Synonym table — short, targeted. Add cases when a real connector
  //    surfaces them, not speculatively.
  const synonyms: Record<string, string[]> = {
    summary:     ['title', 'name', 'subject'],
    description: ['body', 'content', 'desc', 'text'],
    assignee:    ['handler', 'assigned_to', 'owner'],
    reporter:    ['author', 'created_by', 'submitted_by'],
    priority:    ['severity'],
    category:    ['keywords', 'labels', 'tags'],          // mantis: 'keywords' col
    url:         ['web_url', 'link', 'href'],
  };
  for (const alt of synonyms[lc] || []) {
    if (lcMap.has(alt)) return lcMap.get(alt)!;
  }
  return null;
}

export async function POST(req: Request) {
  let body: any;
  try { body = await req.json(); }
  catch { return NextResponse.json({ ok: false, error: 'invalid JSON body' }, { status: 400 }); }

  const { source_connector, source_tool, source_input, items_path, pipeline_inputs } = body || {};
  if (!source_connector || !source_tool) {
    return NextResponse.json({ ok: false, error: 'source_connector and source_tool are required' }, { status: 400 });
  }

  const callName = `${source_connector}.${source_tool}`;
  let toolResult;
  try {
    toolResult = await dispatchTool(
      { id: `jobs-preview-${Date.now()}`, name: callName, input: source_input || {} },
      { noTruncation: true },
    );
  } catch (e) {
    return NextResponse.json({ ok: false, error: `connector call threw: ${(e as Error).message}` }, { status: 500 });
  }

  if (toolResult.is_error) {
    return NextResponse.json({
      ok: false,
      error: `connector returned is_error=true`,
      raw_preview: toolResult.content.slice(0, 800),
    }, { status: 200 });
  }

  let parsed: unknown;
  try { parsed = JSON.parse(toolResult.content); }
  catch {
    return NextResponse.json({
      ok: false,
      error: 'connector returned non-JSON content',
      raw_preview: toolResult.content.slice(0, 800),
    }, { status: 200 });
  }

  // Always compute the shape-based items_path suggestion — UI offers it
  // both on success ("you may also want this") and on failure ("apply
  // this and retry"), so the user never has to guess.
  const suggestedItemsPath = suggestItemsPath(parsed);

  let items = pickPath(parsed, items_path || '');
  // Same single-object-as-1-item-list logic as the scheduler, so the
  // preview matches what runtime will see.
  if (items && typeof items === 'object' && !Array.isArray(items)) {
    items = [items];
  }
  if (!Array.isArray(items) || items.length === 0) {
    const topKeys = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
      ? Object.keys(parsed as Record<string, unknown>)
      : [];
    return NextResponse.json({
      ok: false,
      error: Array.isArray(items)
        ? `items_path resolved to an empty array — no sample item available`
        : `items_path "${items_path || '(empty)'}" did not resolve to an array or object`,
      top_level_keys: topKeys,
      suggested_items_path: suggestedItemsPath,
    }, { status: 200 });
  }

  const sampleItem = items[0] as Record<string, unknown>;
  // Union of keys across ALL returned items, not just items[0]. Mantis
  // bug rows occasionally omit fields when blank — e.g. a bug with no
  // fix_schedule won't have that key on its object — so basing the
  // template on a single sample misses real-world fields. Scan up to
  // 20 items (covers >99% of typical job batches).
  const keySet = new Set<string>();
  const sampleCount = Math.min(items.length, 20);
  for (let i = 0; i < sampleCount; i++) {
    const it = items[i];
    if (it && typeof it === 'object' && !Array.isArray(it)) {
      for (const k of Object.keys(it as Record<string, unknown>)) keySet.add(k);
    }
  }
  const itemKeys = Array.from(keySet);

  let suggestedTemplate: Record<string, string> | undefined;
  if (Array.isArray(pipeline_inputs) && pipeline_inputs.length > 0) {
    suggestedTemplate = {};
    for (const inputName of pipeline_inputs) {
      if (typeof inputName !== 'string') continue;
      const match = suggestMapping(inputName, itemKeys);
      // If no fuzzy match found, leave it blank so the user knows it needs
      // a constant (e.g. base_branch). NEVER guess across unrelated names.
      suggestedTemplate[inputName] = match ? `{{item.${match}}}` : '';
    }
  }

  return NextResponse.json({
    ok: true,
    sample_item: sampleItem,
    item_keys: itemKeys,
    suggested_template: suggestedTemplate,
    suggested_items_path: suggestedItemsPath,
    suggested_dedup_field: suggestDedupField(itemKeys),
    total_items: items.length,
  });
}
