'use strict';
const fs = require('node:fs');
const path = require('node:path');
const { createClient } = require('./llm/client');
const agentRunners = require('./agent-runners');
const { inferAudience } = require('./coding-prompts');

/**
 * Build a prompt for reviewing a single subtask's diff against its intent.
 * @param {{ title: string, prompt: string }} subtask
 * @param {string} diff
 * @returns {string}
 */
function buildSubtaskReviewPrompt(subtask, diff) {
  return `You are a code reviewer. Review the following diff for a subtask and check whether it correctly implements the stated intent. Look for bugs, security issues, and missing pieces.

## Subtask Title
${subtask.title}

## Subtask Intent
${subtask.prompt}

## Diff
\`\`\`
${diff}
\`\`\`

Reply with JSON in exactly this format (no other text outside the JSON):
{"pass": true/false, "issues": ["description of each issue"], "severity": "minor|major|blocker"}

- "pass": true if the diff correctly implements the intent with no significant issues
- "issues": array of strings describing any problems found (empty array if none)
- "severity": "minor" for style/nitpicks, "major" for bugs or missing functionality, "blocker" for security issues or fundamentally broken logic`;
}

const FRONTEND_DIFF_RE = /\.(html?|css|scss|sass|less|jsx|tsx|vue|svelte|astro)\b/i;

function diffTouchesFrontend(diff) {
  if (!diff) return false;
  const lines = String(diff).split('\n');
  return lines.some((line) => /^(diff --git|\+\+\+|---)\s/.test(line) && FRONTEND_DIFF_RE.test(line));
}

const VIEWPORT_LABEL = {
  desktop: 'desktop (1440×900)',
  mobile: 'mobile (390×844)',
  tablet: 'tablet (1024×768)',
};

function loadImageBlock(filePath) {
  if (!filePath || !fs.existsSync(filePath)) return null;
  const ext = path.extname(filePath).toLowerCase().slice(1);
  const mediaType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'image/png';
  const data = fs.readFileSync(filePath).toString('base64');
  return {
    type: 'image',
    source: { type: 'base64', media_type: mediaType, data },
  };
}

function buildVisualReviewPrompt(request, screenshots, audience, fullDiff) {
  const audienceInfo = audience || inferAudience(request);
  const audienceText = audienceInfo
    ? `Audience: ${audienceInfo.audience}.\nIntended register: ${audienceInfo.register}.`
    : 'Audience: not explicitly stated. Infer from the request and grade against the most plausible intended audience.';

  const shotList = (screenshots || [])
    .map((s, i) => `  [${i + 1}] ${VIEWPORT_LABEL[s.viewport] || s.viewport || 'unknown viewport'} — ${s.url || '(unknown url)'}`)
    .join('\n') || '  (no screenshots provided)';

  const diffSummary = fullDiff ? `\n## Diff (for context — focus your review on the SCREENSHOTS, not this)\n\`\`\`\n${String(fullDiff).slice(0, 8000)}\n\`\`\`` : '';

  return `You are a senior product designer doing a final visual review of a UI change. Your job is to catch problems the agent might not have noticed — but you have to calibrate to the site's genre, not apply portfolio rules to a SaaS page or vice versa.

## Original Request
${request}

${audienceText}

## Screenshots attached (in order)
${shotList}

## How to grade

First, infer the site's genre from the request + screenshots. Common genres:
- Personal portfolio (artist / photographer / designer / writer)
- SaaS / product landing page
- B2B / enterprise marketing
- Internal admin tool or dashboard
- Editorial / publication / blog
- Local business / restaurant / shop
- Documentation

The "AI tells" you flag depend on the genre. Don't flag a SaaS landing for having stat cards in the hero — that's correct for SaaS. Don't flag a portfolio for having an editorial book-style serif if the user requested that explicitly. The patterns below are the most-overused tells for the **portfolio genre specifically**:

1. **Audience fit**: does the visual register actually match the intended audience and the site's apparent genre?
2. **Genre-appropriate AI tells**: if the site is a personal portfolio, flag these patterns when 3+ appear together:
   - Cormorant Garamond / Playfair Display + Inter pairing
   - Cream / off-white + forest or sage green palette
   - Stat cards in the hero ("4+ Awards / Founder / 1st Place")
   - Vertical award timeline with dots and years
   - Max-width 1200px contained grid throughout
   - Border-radius 8px on every surface, subtle drop shadow + translateY hover
   - 🏆 / ★ / 🦜 emoji sprinkles on achievements
   For other genres, list the patterns YOU consider overused defaults for that genre and check the same way. Stat cards on a SaaS landing are NOT a tell; stat cards on a 12-year-old's portfolio are.
3. **Image-to-text balance**: for a visual creator's portfolio, is the WORK given visual weight, or is the bio / headshot / stats bigger than the art? (For text-led genres, the inverse — is typography hierarchy strong?)
4. **Visual rhythm**: are sections varied or rhythmically identical from top to bottom?
5. **Mobile**: does the layout hold up at 390px wide? (Check the mobile screenshot specifically — overflow, clipped text, broken grid.)
6. **Voice**: any AI-generic copy that should have been left as a placeholder ("WRITE A REAL BIO HERE") instead of inventing filler?
${diffSummary}

## Output
Reply with ONE JSON object, no other text:
{"pass": true|false, "issues": ["<one issue per array entry, specific>"], "severity": "minor|major|blocker", "summary": "<one sentence overall verdict>"}

Severity rules (apply to the inferred genre, not portfolio assumptions):
- "minor": polish nits (one weak section, one missing micro-interaction)
- "major": clear audience/register mismatch for the inferred genre, OR multiple genre-appropriate AI tells, OR content hierarchy inverted for the genre
- "blocker": template drift across the entire page in a way that contradicts the inferred audience, copy obviously AI-generic, mobile layout broken
- "pass: true" when register matches the audience AND no significant genre-specific drift`;
}

async function runVisionReview(prompt, screenshots, reviewer, opts = {}) {
  const provider = reviewer.provider || opts.provider || createClient(reviewer.providerType || 'anthropic', {
    apiKey: reviewer.apiKey,
    baseUrl: reviewer.baseUrl,
  });

  const imageBlocks = (screenshots || [])
    .map((s) => loadImageBlock(s?.path))
    .filter(Boolean);

  const content = [
    { type: 'text', text: prompt },
    ...imageBlocks,
  ];

  const response = await provider.chat({
    model: reviewer.model || opts.model,
    messages: [{ role: 'user', content }],
    maxTokens: opts.maxTokens || 2048,
    temperature: 0,
  });

  return {
    text: response.content || '',
    providerType: response.provider || reviewer.providerType || provider.type,
    model: response.model || reviewer.model,
    imageCount: imageBlocks.length,
  };
}

/**
 * Build a prompt for final review of the entire diff against the original request.
 * @param {string} request - The original user request
 * @param {string} fullDiff - Combined diff of all changes
 * @returns {string}
 */
function buildFinalReviewPrompt(request, fullDiff) {
  return `You are a senior code reviewer performing a final review of an entire changeset. Check that the diff fully implements the original request. Evaluate:

1. Cross-file consistency - do imports, exports, and interfaces line up?
2. Missing pieces - is anything from the request left unimplemented?
3. Architectural issues - are there design problems that span multiple files?
4. Bugs and security issues

## Original Request
${request}

## Full Diff
\`\`\`
${fullDiff}
\`\`\`

Reply with JSON in exactly this format (no other text outside the JSON):
{"pass": true/false, "issues": ["description of each issue"], "severity": "minor|major|blocker", "summary": "one-line summary of the review"}

- "pass": true if the changeset correctly and completely implements the request
- "issues": array of strings describing any problems found (empty array if none)
- "severity": "minor" for style/nitpicks, "major" for bugs or missing functionality, "blocker" for security issues or fundamentally broken logic
- "summary": a brief one-line summary of the overall review`;
}

/**
 * Extract a JSON verdict from Claude's response text.
 * Finds the first JSON object containing a "pass" field.
 * @param {string} text
 * @returns {{ pass: boolean, issues: string[], severity: string, summary: string }}
 */
function parseVerdict(text) {
  const failResult = {
    pass: false,
    issues: ['Failed to parse review verdict'],
    severity: 'blocker',
    summary: '',
  };

  if (!text) return failResult;

  // Match all JSON-like objects in the text
  const jsonPattern = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
  let match;
  while ((match = jsonPattern.exec(text)) !== null) {
    try {
      const parsed = JSON.parse(match[0]);
      if ('pass' in parsed) {
        return {
          pass: Boolean(parsed.pass),
          issues: Array.isArray(parsed.issues) ? parsed.issues : [],
          severity: parsed.severity || 'minor',
          summary: parsed.summary || '',
        };
      }
    } catch {
      // Not valid JSON, try next match
    }
  }

  return failResult;
}

/**
 * Pick a reviewer. Prefer a reviewer from a different provider than the worker.
 * @param {{ workerProvider?: string, reviewer?: object, reviewers?: object[] }} opts
 * @returns {object}
 */
function selectReviewer(opts = {}) {
  if (opts.reviewer) return opts.reviewer;
  const reviewers = Array.isArray(opts.reviewers) ? opts.reviewers.filter(Boolean) : [];
  if (reviewers.length > 0) {
    const workerProvider = opts.workerProvider || null;
    return reviewers.find(r => r.providerType && r.providerType !== workerProvider) || reviewers[0];
  }
  return {
    kind: 'agent',
    runnerId: 'claude-code',
    providerType: 'anthropic',
    model: opts.model,
  };
}

async function runModelReview(prompt, reviewer, opts = {}) {
  const provider = reviewer.provider || opts.provider || createClient(reviewer.providerType, {
    apiKey: reviewer.apiKey,
    baseUrl: reviewer.baseUrl,
  });
  const response = await provider.chat({
    model: reviewer.model || opts.model,
    messages: [{ role: 'user', content: prompt }],
    maxTokens: opts.maxTokens || 2048,
    temperature: 0,
  });
  return {
    text: response.content || '',
    providerType: response.provider || reviewer.providerType || provider.type,
    model: response.model || reviewer.model,
  };
}

async function runAgentReview(prompt, reviewer, opts = {}) {
  const runnerId = reviewer.runnerId || reviewer.id || 'claude-code';
  const result = await agentRunners.runAgentTask(runnerId, {
    prompt,
    cwd: opts.cwd,
    timeoutMs: opts.timeoutMs || 120_000,
    model: reviewer.model || opts.model,
    mode: 'review',
    outputFormat: reviewer.outputFormat || 'text',
    maxTurns: reviewer.maxTurns || 1,
    permissionMode: reviewer.permissionMode,
  });
  if (!result.success && !result.output) {
    throw new Error(`${runnerId} exited with code ${result.exitCode}: ${result.stderr || 'no output'}`);
  }
  return {
    text: result.output || '',
    providerType: result.providerType || reviewer.providerType,
    model: reviewer.model,
    runnerId,
  };
}

/**
 * Run a review prompt and return the parsed verdict.
 * @param {string} prompt
 * @param {{ cwd?: string, timeoutMs?: number, reviewer?: object, reviewers?: object[], workerProvider?: string }} opts
 * @returns {Promise<{ pass: boolean, issues: string[], severity: string, summary: string, reviewer?: object }>}
 */
async function runReview(prompt, opts = {}) {
  const reviewer = selectReviewer(opts);
  const blockerVerdict = {
    pass: false,
    issues: ['Review process failed'],
    severity: 'blocker',
    summary: '',
    reviewer,
  };

  try {
    const kind = reviewer.kind || (reviewer.runnerId || reviewer.id ? 'agent' : 'model');
    const review = kind === 'model'
      ? await runModelReview(prompt, reviewer, opts)
      : await runAgentReview(prompt, reviewer, opts);
    return {
      ...parseVerdict(review.text),
      reviewer: {
        kind,
        providerType: review.providerType || reviewer.providerType,
        model: review.model || reviewer.model,
        runnerId: review.runnerId || reviewer.runnerId,
      },
    };
  } catch (err) {
    blockerVerdict.issues = [`Review error: ${err.message}`];
    return blockerVerdict;
  }
}

/**
 * Review a single subtask's diff.
 * @param {{ title: string, prompt: string }} subtask
 * @param {string} diff
 * @param {{ cwd?: string, timeoutMs?: number, budgetUsd?: number }} opts
 * @returns {Promise<{ pass: boolean, issues: string[], severity: string, summary: string }>}
 */
function reviewSubtask(subtask, diff, opts = {}) {
  const prompt = buildSubtaskReviewPrompt(subtask, diff);
  return runReview(prompt, opts);
}

/**
 * Final review of the entire diff against the original request.
 * @param {string} request
 * @param {string} fullDiff
 * @param {{ cwd?: string, timeoutMs?: number, budgetUsd?: number }} opts
 * @returns {Promise<{ pass: boolean, issues: string[], severity: string, summary: string }>}
 */
function reviewFinal(request, fullDiff, opts = {}) {
  const prompt = buildFinalReviewPrompt(request, fullDiff);
  return runReview(prompt, opts);
}

/**
 * Visual design review of the rendered UI. Only runs when frontend files
 * changed AND screenshots were captured. Returns the same verdict shape
 * as reviewFinal so the orchestrator can combine results uniformly.
 *
 * @param {string} request
 * @param {Array<{path: string, viewport?: string, url?: string}>} screenshots
 * @param {string} fullDiff
 * @param {object} opts - { cwd, timeoutMs, audience, reviewer, reviewers, workerProvider }
 */
async function reviewVisual(request, screenshots, fullDiff, opts = {}) {
  if (!Array.isArray(screenshots) || screenshots.length === 0) {
    return {
      pass: true,
      issues: [],
      severity: 'minor',
      summary: 'Visual review skipped: no screenshots captured',
      skipped: true,
    };
  }

  const validScreenshots = screenshots.filter((s) => s && s.path && fs.existsSync(s.path));
  if (validScreenshots.length === 0) {
    return {
      pass: true,
      issues: [],
      severity: 'minor',
      summary: 'Visual review skipped: all screenshot paths missing on disk',
      skipped: true,
    };
  }

  const prompt = buildVisualReviewPrompt(request, validScreenshots, opts.audience, fullDiff);
  const reviewer = selectReviewer({ ...opts, reviewer: opts.reviewer });

  const blockerVerdict = {
    pass: false,
    issues: ['Visual review process failed'],
    severity: 'blocker',
    summary: '',
    reviewer,
  };

  try {
    const review = await runVisionReview(prompt, validScreenshots, reviewer, opts);
    return {
      ...parseVerdict(review.text),
      reviewer: {
        kind: 'model',
        providerType: review.providerType || reviewer.providerType,
        model: review.model || reviewer.model,
      },
      screenshotsReviewed: validScreenshots.length,
    };
  } catch (err) {
    blockerVerdict.issues = [`Visual review error: ${err.message}`];
    return blockerVerdict;
  }
}

module.exports = {
  buildSubtaskReviewPrompt,
  buildFinalReviewPrompt,
  buildVisualReviewPrompt,
  diffTouchesFrontend,
  loadImageBlock,
  parseVerdict,
  selectReviewer,
  runModelReview,
  runAgentReview,
  runVisionReview,
  runReview,
  reviewSubtask,
  reviewFinal,
  reviewVisual,
  FRONTEND_DIFF_RE,
};
