const path = require('node:path');
const { parseBboxes, normalizePartName, runWithConcurrency } = require('./json_utils.cjs');
const { callVisionApi, imageContent } = require('./vision_client.cjs');
const { emitEvent, writeJson, toRelative } = require('./workspace.cjs');
const { markImage } = require('./image_marker.cjs');

function nearestFrames(frames, timestamp, windowSec, limit = 5) {
  const selected = frames.filter(frame => Math.abs(frame.timestamp - timestamp) <= windowSec);
  const source = selected.length > 0
    ? selected
    : frames.slice().sort((a, b) => Math.abs(a.timestamp - timestamp) - Math.abs(b.timestamp - timestamp));
  if (source.length <= limit) return source;
  const sorted = source.slice().sort((a, b) => Math.abs(a.timestamp - timestamp) - Math.abs(b.timestamp - timestamp));
  const picked = [];
  for (const frame of sorted) {
    picked.push(frame);
    if (picked.length >= limit) break;
  }
  return picked.sort((a, b) => a.timestamp - b.timestamp);
}

function parseBboxesByTimestamp(text, timestamps) {
  const output = new Map();
  for (const ts of timestamps) {
    const variants = [ts.toFixed(1), ts.toFixed(2)].map(item => item.replace('.', '\\.'));
    const re = new RegExp(`\\[\\s*(?:${variants.join('|')})\\s*second\\s*\\]\\s*<bbox>\\s*([0-9.]+)\\s+([0-9.]+)\\s+([0-9.]+)\\s+([0-9.]+)\\s*<\\/bbox>`, 'i');
    const match = String(text || '').match(re);
    if (match) {
      output.set(ts, {
        x1: Number(match[1]),
        y1: Number(match[2]),
        x2: Number(match[3]),
        y2: Number(match[4]),
      });
    }
  }
  if (output.size === 0) {
    const boxes = parseBboxes(text);
    if (boxes.length === 1) {
      for (const ts of timestamps) {
        output.set(ts, { x1: boxes[0][0], y1: boxes[0][1], x2: boxes[0][2], y2: boxes[0][3] });
      }
    } else {
      boxes.slice(0, timestamps.length).forEach((box, index) => {
        output.set(timestamps[index], { x1: box[0], y1: box[1], x2: box[2], y2: box[3] });
      });
    }
  }
  return output;
}

async function groundOne({ workspace, frames, candidate, index, modelConfig, prompt, groundingWindow, frameLimit }) {
  const nearby = nearestFrames(frames, candidate.timestamp, groundingWindow, frameLimit);
  emitEvent('grounding', 'grounding candidate frames selected', {
    candidateId: candidate.id,
    frameCount: nearby.length,
    timestamp: candidate.timestamp,
  });
  const content = [];
  for (const frame of nearby) {
    content.push({ type: 'text', text: `[${frame.timestamp.toFixed(2)} second]` });
    content.push(imageContent(frame.path));
  }
  const part = normalizePartName(candidate.part);
  content.push({
    type: 'text',
    text: `${prompt}\n损伤：${part} ${candidate.type}。描述：${candidate.description}\n请只框真实损伤本体。`,
  });
  const result = await callVisionApi({
    config: modelConfig,
    model: modelConfig.groundingModel,
    messages: [{ role: 'user', content }],
    maxTokens: 2048,
    temperature: 0.1,
  });
  const bboxMap = result.ok ? parseBboxesByTimestamp(result.content, nearby.map(frame => frame.timestamp)) : new Map();
  const damageId = `damage_${String(index + 1).padStart(3, '0')}`;
  const markedFrames = [];
  for (const frame of nearby) {
    const bbox = bboxMap.get(frame.timestamp) || null;
    const markedName = `${damageId}_${frame.timestamp.toFixed(2).replace('.', '_')}s.jpg`;
    const markedPath = path.join(workspace.workspacePath, 'marked_frames', markedName);
    const markResult = await markImage({
      sourcePath: frame.path,
      targetPath: markedPath,
      bbox,
      label: `${part}${candidate.type}`,
    });
    markedFrames.push({
      timestamp: frame.timestamp,
      path: markedPath,
      sourcePath: frame.path,
      sourceRelativePath: frame.relativePath,
      markedRelativePath: toRelative(workspace, markedPath),
      bbox: bbox ? { normalized: bbox, pixel: markResult.pixel } : null,
      groundingRaw: result.content || result.error || '',
      groundingStatus: bbox ? 'grounded' : 'no_bbox',
    });
  }
  return {
    candidateId: candidate.id,
    damageId,
    part,
    type: candidate.type,
    severity: candidate.severity,
    description: candidate.description,
    confidence: candidate.confidence,
    uncertain: candidate.uncertain,
    uncertainReason: candidate.uncertainReason,
    markedFrames,
  };
}

async function groundDamages({ workspace, frames, candidates, modelConfig, prompt, groundingWindow = 2, frameLimit = 5, concurrency = 3 }) {
  emitEvent('grounding', 'grounding damage candidates', { candidateCount: candidates.length, groundingWindow, frameLimit, concurrency, model: modelConfig.groundingModel });
  const tasks = candidates.map((candidate, index) => () => groundOne({
      workspace,
      frames,
      candidate,
      index,
      modelConfig,
      prompt,
      groundingWindow,
      frameLimit,
    }));
  const annotations = await runWithConcurrency(tasks, concurrency);
  writeJson(workspace, 'damage_annotations.json', { annotations });
  emitEvent('grounding', 'grounding complete', { annotationCount: annotations.length });
  return { annotations };
}

module.exports = {
  nearestFrames,
  parseBboxesByTimestamp,
  groundDamages,
};
