import * as fs from 'fs';
import * as path from 'path';
import { ToolDef, type Message } from '../../llm/openai-compat.js';
import type { ToolContext, ToolResult } from './core.js';
import { resolveAndGuard, resolveOutputPathWithin } from './core.js';

type ReviewOutput = {
  source_file: string;
  source_image?: string;
  summary: string;
  quality: 'good' | 'partial' | 'poor' | 'unknown';
  confidence?: 'high' | 'medium' | 'low' | 'unknown';
  needs_retry: boolean;
  extracted_items: Record<string, unknown>;
  missing_items: string[];
  notes: string[];
  corrected_text?: string;
  issues?: Array<{
    type: string;
    before?: string;
    after?: string;
    reason?: string;
  }>;
};

const BATCH_REVIEW_TEXT_WITH_LLM_DEF: ToolDef = {
  type: 'function',
  function: {
    name: 'BatchReviewTextWithLLM',
    description: '複数のテキストファイルを独立した LLM 呼び出しで個別評価し、JSON または Markdown の結果群を生成する。',
    parameters: {
      type: 'object',
      properties: {
        input_glob: { type: 'string', description: 'workspace 基準の glob パターン（例: output/ocr/*.md）' },
        review_prompt: { type: 'string', description: '各ファイルに対して行う評価・抽出・判定の指示' },
        output_dir: { type: 'string', description: '出力先ディレクトリ（省略時: output/reviewed）' },
        output_format: { type: 'string', enum: ['json', 'md'], description: '各ファイルの出力形式（省略時: json）' },
        max_chars_per_file: { type: 'number', description: '各ファイルから LLM に渡す最大文字数（省略時: 20000）' },
      },
      required: ['input_glob', 'review_prompt'],
    },
  },
};

const MERGE_REVIEWED_RESULTS_DEF: ToolDef = {
  type: 'function',
  function: {
    name: 'MergeReviewedResults',
    description: 'レビュー済み JSON を集約し、最終 Markdown レポートを生成する。merge_prompt を指定すると最終集約だけ追加で LLM を使う。',
    parameters: {
      type: 'object',
      properties: {
        input_glob: { type: 'string', description: 'レビュー済み JSON の glob パターン（例: output/reviewed/*.json）' },
        output_path: { type: 'string', description: '最終 Markdown の出力パス（省略時: output/reports/review-summary.md）' },
        merge_prompt: { type: 'string', description: '最終レポートを LLM で整形する場合の追加指示（省略可）' },
      },
      required: ['input_glob'],
    },
  },
};

export const TOOL_DEFS: Record<string, ToolDef> = {
  BatchReviewTextWithLLM: BATCH_REVIEW_TEXT_WITH_LLM_DEF,
  MergeReviewedResults: MERGE_REVIEWED_RESULTS_DEF,
};

function globToRegExp(pattern: string): RegExp {
  const regexStr = pattern
    .replace(/[.+^${}()|[\]\\]/g, '\\$&')
    .replace(/\*\*/g, '::DOUBLE_STAR::')
    .replace(/\*/g, '[^/]*')
    .replace(/\?/g, '.')
    .replace(/::DOUBLE_STAR::/g, '.*');
  return new RegExp(`^${regexStr}$`);
}

function collectFiles(dir: string, base: string): string[] {
  const results: string[] = [];
  let entries: fs.Dirent[];
  try {
    entries = fs.readdirSync(dir, { withFileTypes: true });
  } catch {
    return results;
  }

  for (const entry of entries) {
    const full = path.join(dir, entry.name);
    const rel = path.relative(base, full);
    if (entry.isDirectory()) {
      results.push(...collectFiles(full, base));
    } else {
      results.push(rel);
    }
  }
  return results;
}

function sanitizeBaseName(name: string): string {
  return name.replace(/[\\/:*?"<>|]/g, '_').replace(/\s+/g, '_').replace(/\.+$/g, '') || 'review';
}

function extractJsonBlock(text: string): string {
  const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
  if (fenced?.[1]) return fenced[1].trim();
  return text.trim();
}

function normalizeReviewOutput(sourceFile: string, raw: unknown): ReviewOutput {
  const value = (raw && typeof raw === 'object') ? raw as Record<string, unknown> : {};
  const extractedItems = value['extracted_items'];
  const issues = Array.isArray(value['issues'])
    ? value['issues']
        .filter((issue): issue is Record<string, unknown> => Boolean(issue) && typeof issue === 'object')
        .map((issue) => ({
          type: typeof issue['type'] === 'string' ? issue['type'] : 'unknown',
          before: typeof issue['before'] === 'string' ? issue['before'] : undefined,
          after: typeof issue['after'] === 'string' ? issue['after'] : undefined,
          reason: typeof issue['reason'] === 'string' ? issue['reason'] : undefined,
        }))
    : [];
  return {
    source_file: typeof value['source_file'] === 'string' ? value['source_file'] : sourceFile,
    source_image: typeof value['source_image'] === 'string' ? value['source_image'] : undefined,
    summary: typeof value['summary'] === 'string' ? value['summary'] : '',
    quality: value['quality'] === 'good' || value['quality'] === 'partial' || value['quality'] === 'poor'
      ? value['quality']
      : 'unknown',
    confidence: value['confidence'] === 'high' || value['confidence'] === 'medium' || value['confidence'] === 'low'
      ? value['confidence']
      : 'unknown',
    needs_retry: typeof value['needs_retry'] === 'boolean' ? value['needs_retry'] : false,
    extracted_items: extractedItems && typeof extractedItems === 'object' && !Array.isArray(extractedItems)
      ? extractedItems as Record<string, unknown>
      : {},
    missing_items: Array.isArray(value['missing_items']) ? value['missing_items'].map(String) : [],
    notes: Array.isArray(value['notes']) ? value['notes'].map(String) : [],
    corrected_text: typeof value['corrected_text'] === 'string' ? value['corrected_text'] : undefined,
    issues,
  };
}

function renderReviewMarkdown(review: ReviewOutput): string {
  const extractedLines = Object.entries(review.extracted_items).length > 0
    ? Object.entries(review.extracted_items).map(([key, value]) => `- ${key}: ${JSON.stringify(value)}`).join('\n')
    : '- (none)';

  return [
    `# Review: ${path.basename(review.source_file)}`,
    '',
    `- Source: \`${review.source_file}\``,
    review.source_image ? `- Source image: \`${review.source_image}\`` : null,
    `- Quality: ${review.quality}`,
    review.confidence ? `- Confidence: ${review.confidence}` : null,
    `- Needs retry: ${review.needs_retry ? 'yes' : 'no'}`,
    '',
    '## Summary',
    '',
    review.summary || '(empty)',
    '',
    '## Extracted Items',
    '',
    extractedLines,
    '',
    '## Missing Items',
    '',
    review.missing_items.length > 0 ? review.missing_items.map((item) => `- ${item}`).join('\n') : '- (none)',
    '',
    '## Notes',
    '',
    review.notes.length > 0 ? review.notes.map((item) => `- ${item}`).join('\n') : '- (none)',
    '',
    '## Issues',
    '',
    review.issues && review.issues.length > 0
      ? review.issues.map((issue) => `- ${issue.type}${issue.before ? ` | before: ${issue.before}` : ''}${issue.after ? ` | after: ${issue.after}` : ''}${issue.reason ? ` | reason: ${issue.reason}` : ''}`).join('\n')
      : '- (none)',
    '',
    '## Corrected Text',
    '',
    review.corrected_text || '(none)',
    '',
  ].filter((line): line is string => line !== null).join('\n');
}

function renderMergedMarkdown(reviews: ReviewOutput[]): string {
  const needsRetry = reviews.filter((review) => review.needs_retry);

  return [
    '# Reviewed Results Summary',
    '',
    `- Files processed: ${reviews.length}`,
    `- Needs retry: ${needsRetry.length}`,
    '',
    '## Retry Candidates',
    '',
    needsRetry.length > 0
      ? needsRetry.map((review) => `- \`${review.source_file}\`: ${review.summary || review.notes.join('; ') || 'needs retry'}`).join('\n')
      : '- (none)',
    '',
    '## Per File Details',
    '',
    ...reviews.flatMap((review) => [
      `### ${path.basename(review.source_file)}`,
      '',
      `- Source: \`${review.source_file}\``,
      review.source_image ? `- Source image: \`${review.source_image}\`` : null,
      `- Quality: ${review.quality}`,
      review.confidence ? `- Confidence: ${review.confidence}` : null,
      `- Needs retry: ${review.needs_retry ? 'yes' : 'no'}`,
      '',
      'Summary:',
      review.summary || '(empty)',
      '',
      'Extracted items:',
      Object.entries(review.extracted_items).length > 0
        ? Object.entries(review.extracted_items).map(([key, value]) => `- ${key}: ${JSON.stringify(value)}`).join('\n')
        : '- (none)',
      '',
      'Missing items:',
      review.missing_items.length > 0 ? review.missing_items.map((item) => `- ${item}`).join('\n') : '- (none)',
      '',
      'Notes:',
      review.notes.length > 0 ? review.notes.map((item) => `- ${item}`).join('\n') : '- (none)',
      '',
      'Issues:',
      review.issues && review.issues.length > 0
        ? review.issues.map((issue) => `- ${issue.type}${issue.before ? ` | before: ${issue.before}` : ''}${issue.after ? ` | after: ${issue.after}` : ''}${issue.reason ? ` | reason: ${issue.reason}` : ''}`).join('\n')
        : '- (none)',
      '',
      'Corrected text:',
      review.corrected_text || '(none)',
      '',
    ].filter((line): line is string => line !== null)),
  ].join('\n');
}

function readMatchedFiles(globPattern: string, workspacePath: string): string[] {
  const matcher = globToRegExp(globPattern);
  return collectFiles(workspacePath, workspacePath)
    .filter((rel) => matcher.test(rel))
    .sort((left, right) => left.localeCompare(right));
}

async function reviewFileWithLlm(
  sourceFile: string,
  content: string,
  reviewPrompt: string,
  ctx: ToolContext,
): Promise<ReviewOutput> {
  if (!ctx.runIsolatedLlm) {
    throw new Error('BatchReviewTextWithLLM requires isolated LLM execution support');
  }

  const messages: Message[] = [
    {
      role: 'system',
      content: [
        'あなたは structured reviewer です。',
        '1 ファイルずつ読み込み、JSON のみを返してください。',
        '以下のキーを持つ JSON オブジェクトを返すこと:',
        'source_file (string), summary (string), quality ("good" | "partial" | "poor" | "unknown"), needs_retry (boolean), extracted_items (object), missing_items (string array), notes (string array)。',
        '追加で許可されるキー: source_image (string), confidence ("high" | "medium" | "low" | "unknown"), corrected_text (string), issues (array)。',
        'JSON を散文で包まないこと。',
      ].join('\n'),
    },
    {
      role: 'user',
      content: [
        `Review prompt:\n${reviewPrompt}`,
        '',
        `Source file: ${sourceFile}`,
        '',
        'File content:',
        content,
      ].join('\n'),
    },
  ];

  const raw = await ctx.runIsolatedLlm(messages);
  try {
    return normalizeReviewOutput(sourceFile, JSON.parse(extractJsonBlock(raw)) as unknown);
  } catch {
    return {
      source_file: sourceFile,
      summary: raw.trim(),
      quality: 'unknown',
      needs_retry: true,
      extracted_items: {},
      missing_items: [],
      notes: ['LLM response was not valid JSON'],
    };
  }
}

async function executeBatchReviewTextWithLlm(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  if (!ctx.editAllowed) {
    return { output: 'BatchReviewTextWithLLM is not allowed: edit flag is false', isError: true };
  }

  const inputGlob = input['input_glob'] as string;
  const reviewPrompt = input['review_prompt'] as string;
  const outputDir = typeof input['output_dir'] === 'string' ? input['output_dir'] : 'output/reviewed';
  const outputFormat = input['output_format'] === 'md' ? 'md' : 'json';
  const maxCharsPerFile = typeof input['max_chars_per_file'] === 'number' ? input['max_chars_per_file'] : 20_000;

  const matched = readMatchedFiles(inputGlob, ctx.workspacePath);
  if (matched.length === 0) {
    return { output: `No text files matched: ${inputGlob}`, isError: true };
  }

  let resolvedOutputDir: string;
  try {
    resolvedOutputDir = resolveOutputPathWithin(ctx.workspacePath, outputDir, ['output/reviewed']);
    fs.mkdirSync(resolvedOutputDir, { recursive: true });
  } catch (e) {
    return { output: `Failed to prepare output directory: ${(e as Error).message}`, isError: true };
  }

  const reviews: ReviewOutput[] = [];
  for (const relPath of matched) {
    let resolvedInput: string;
    try {
      resolvedInput = resolveAndGuard(ctx.workspacePath, relPath);
    } catch (e) {
      return { output: (e as Error).message, isError: true };
    }

    let content: string;
    try {
      content = fs.readFileSync(resolvedInput, 'utf-8');
    } catch (e) {
      return { output: `Failed to read ${relPath}: ${(e as Error).message}`, isError: true };
    }

    const review = await reviewFileWithLlm(relPath, content.slice(0, maxCharsPerFile), reviewPrompt, ctx);
    reviews.push(review);

    const baseName = sanitizeBaseName(path.parse(relPath).dir
      ? `${path.parse(relPath).dir}__${path.parse(relPath).name}`
      : path.parse(relPath).name);
    const outPath = path.join(resolvedOutputDir, `${baseName}.${outputFormat}`);
    try {
      fs.writeFileSync(
        outPath,
        outputFormat === 'json'
          ? `${JSON.stringify(review, null, 2)}\n`
          : renderReviewMarkdown(review),
        'utf-8',
      );
    } catch (e) {
      return { output: `Failed to write review for ${relPath}: ${(e as Error).message}`, isError: true };
    }
  }

  const manifestPath = path.join(resolvedOutputDir, 'manifest.json');
  try {
    fs.writeFileSync(manifestPath, `${JSON.stringify(reviews, null, 2)}\n`, 'utf-8');
  } catch (e) {
    return { output: `Failed to write manifest: ${(e as Error).message}`, isError: true };
  }

  return {
    output: `Reviewed ${reviews.length} files into ${outputDir}. Manifest: ${path.posix.join(outputDir, 'manifest.json')}`,
    isError: false,
  };
}

async function executeMergeReviewedResults(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  if (!ctx.editAllowed) {
    return { output: 'MergeReviewedResults is not allowed: edit flag is false', isError: true };
  }

  const inputGlob = input['input_glob'] as string;
  const outputPath = typeof input['output_path'] === 'string' ? input['output_path'] : 'output/reports/review-summary.md';
  const mergePrompt = typeof input['merge_prompt'] === 'string' ? input['merge_prompt'] : '';

  const matched = readMatchedFiles(inputGlob, ctx.workspacePath)
    .filter((rel) => rel.endsWith('.json'))
    .filter((rel) => path.basename(rel) !== 'manifest.json');
  if (matched.length === 0) {
    return { output: `No reviewed JSON files matched: ${inputGlob}`, isError: true };
  }

  const reviews: ReviewOutput[] = [];
  for (const relPath of matched) {
    let resolvedInput: string;
    try {
      resolvedInput = resolveAndGuard(ctx.workspacePath, relPath);
    } catch (e) {
      return { output: (e as Error).message, isError: true };
    }

    try {
      const raw = fs.readFileSync(resolvedInput, 'utf-8');
      reviews.push(normalizeReviewOutput(relPath, JSON.parse(raw) as unknown));
    } catch (e) {
      return { output: `Failed to parse review JSON ${relPath}: ${(e as Error).message}`, isError: true };
    }
  }

  let markdown = renderMergedMarkdown(reviews);
  if (mergePrompt && ctx.runIsolatedLlm) {
    const llmOutput = await ctx.runIsolatedLlm([
      {
        role: 'system',
        content: 'あなたはレポート合成アシスタントです。Markdown のみを返してください。',
      },
      {
        role: 'user',
        content: [
          `Merge prompt:\n${mergePrompt}`,
          '',
          'Reviewed JSON records:',
          JSON.stringify(reviews, null, 2),
        ].join('\n'),
      },
    ]);
    if (llmOutput.trim()) {
      markdown = llmOutput.trim();
    }
  }

  let resolvedOutput: string;
  try {
    resolvedOutput = resolveOutputPathWithin(ctx.workspacePath, outputPath, ['output/reports']);
    fs.mkdirSync(path.dirname(resolvedOutput), { recursive: true });
    fs.writeFileSync(resolvedOutput, `${markdown}\n`, 'utf-8');
  } catch (e) {
    return { output: `Failed to write merged report: ${(e as Error).message}`, isError: true };
  }

  return {
    output: `Merged ${reviews.length} reviewed files into ${outputPath}`,
    isError: false,
  };
}

export async function executeTool(
  name: string,
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult | null> {
  switch (name) {
    case 'BatchReviewTextWithLLM':
      return executeBatchReviewTextWithLlm(input, ctx);
    case 'MergeReviewedResults':
      return executeMergeReviewedResults(input, ctx);
    default:
      return null;
  }
}
