'use strict';
const path = require('path');
const fs = require('fs');
const db = require('./db');
const gitUtils = require('./git-utils');
const documentReview = require('./lib/document-review');

const REVIEW_SEVERITIES = new Set(['comment', 'suggestion', 'issue', 'nit', 'question']);
const REVIEW_STATUSES = new Set(['open', 'resolved']);
const REVIEW_SIDES = new Set(['old', 'new']);

// --- Helpers ---
function isValidProjectPath(p) {
  if (!p || typeof p !== 'string') return false;
  const resolved = path.resolve(p);
  const home = process.env.HOME || '/';
  if (!resolved.startsWith(home + path.sep)) return false;
  // Check for .git dir (standalone repo) or use git rev-parse (subdirectory of a repo)
  if (fs.existsSync(path.join(resolved, '.git'))) return true;
  try {
    const { execFileSync } = require('child_process');
    execFileSync('git', ['rev-parse', '--git-dir'], { cwd: resolved, stdio: 'pipe' });
    return true;
  } catch { return false; }
}
function jsonResponse(res, code, data) {
  const body = JSON.stringify(data);
  res.writeHead(code, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
  res.end(body);
}

function reviewErrorPayload(err) {
  const payload = { error: err.message || String(err) };
  if (err.code) payload.code = err.code;
  if (Array.isArray(err.candidates)) payload.candidates = err.candidates;
  return payload;
}

function readBody(req, limit = 1024 * 1024) {
  return new Promise((resolve, reject) => {
    let body = '';
    req.on('data', c => {
      body += c;
      if (body.length > limit) { req.destroy(); reject(new Error('Body too large')); }
    });
    req.on('end', () => { try { resolve(JSON.parse(body || '{}')); } catch (e) { reject(e); } });
    req.on('error', reject);
  });
}

function normalizeReviewSeverity(value) {
  value = String(value || 'comment').trim().toLowerCase();
  return REVIEW_SEVERITIES.has(value) ? value : 'comment';
}

function normalizeReviewStatus(value) {
  value = String(value || 'open').trim().toLowerCase();
  if (!REVIEW_STATUSES.has(value)) throw new Error('Invalid review comment status');
  return value;
}

function normalizeReviewSide(value) {
  value = String(value || 'new').trim().toLowerCase();
  return REVIEW_SIDES.has(value) ? value : 'new';
}

function positiveLineNumber(value, fieldName) {
  const n = Number(value);
  if (!Number.isInteger(n) || n < 1) throw new Error(`${fieldName} must be a positive integer`);
  return n;
}

function sanitizeReviewCommentInput(reviewId, body) {
  body = body || {};
  const filePath = String(body.file_path || '').trim();
  const text = String(body.body || '').trim();
  if (!filePath) throw new Error('file_path required');
  if (!text) throw new Error('body required');
  if (filePath.length > 4096) throw new Error('file_path too long');
  if (text.length > 20000) throw new Error('body too long');
  const lineStart = positiveLineNumber(body.line_start, 'line_start');
  let lineEnd = body.line_end == null || body.line_end === '' ? lineStart : positiveLineNumber(body.line_end, 'line_end');
  if (lineEnd < lineStart) lineEnd = lineStart;
  const out = {
    review_id: reviewId,
    file_path: filePath,
    line_start: lineStart,
    line_end: lineEnd,
    side: normalizeReviewSide(body.side),
    body: text,
    severity: normalizeReviewSeverity(body.severity),
    ai_generated: !!body.ai_generated,
  };
  const anchorKind = String(body.anchor_kind || '').trim().toLowerCase();
  if (['line', 'block', 'file', 'page'].includes(anchorKind)) out.anchor_kind = anchorKind;
  const blockId = String(body.block_id || '').trim();
  if (blockId) out.block_id = blockId.slice(0, 512);
  return out;
}

function sanitizeReviewCommentUpdate(body) {
  body = body || {};
  const update = {};
  if (Object.prototype.hasOwnProperty.call(body, 'body')) {
    const text = String(body.body || '').trim();
    if (!text) throw new Error('body required');
    if (text.length > 20000) throw new Error('body too long');
    update.body = text;
  }
  if (Object.prototype.hasOwnProperty.call(body, 'severity')) {
    update.severity = normalizeReviewSeverity(body.severity);
  }
  if (Object.prototype.hasOwnProperty.call(body, 'status')) {
    update.status = normalizeReviewStatus(body.status);
  }
  return update;
}

// --- Change Detection for Badge Polling ---
// Track last known diff stats per project to detect changes
const changeTracker = new Map(); // projectPath -> { files: [...], hash: string, checkedAt: number }

function computeStatsHash(files) {
  return files.map(f => `${f.path}:${f.additions}:${f.deletions}`).sort().join('|');
}

async function checkForChanges(projectPath, options = {}) {
  try {
    const now = Date.now();
    const ttlMs = Math.max(0, Number(options.ttlMs || 0));
    const prev = changeTracker.get(projectPath);
    if (!options.force && ttlMs > 0 && prev && prev.checkedAt && now - prev.checkedAt < ttlMs) {
      return { changed: false, fileCount: prev.files.length, files: prev.files, cached: true };
    }
    // Use the default review base (all branch work vs main, fallback all-uncommitted) so the
    // chooser badge matches exactly what opening the project shows. Previously this counted
    // only unstaged working-tree changes, which disagreed with the diff view whenever work
    // was staged or committed on a feature branch.
    const { files } = await gitUtils.getDefaultDiffStat(projectPath);
    const hash = computeStatsHash(files);
    changeTracker.set(projectPath, { files, hash, checkedAt: Date.now() });
    const changed = !prev || prev.hash !== hash;
    return { changed, fileCount: files.length, files };
  } catch {
    return { changed: false, fileCount: 0, files: [] };
  }
}

// Resolve a COMPARE base sentinel to a unified diff (file list). Sentinels:
//   ''            working tree vs HEAD (unstaged only)
//   '--staged'    staged changes
//   '--uncommitted' (or 'HEAD')  all uncommitted = staged + unstaged
//   '--vs-main'   all branch work: working tree vs merge-base(main, HEAD)
//   <sha>         a single commit's parent..commit diff
async function diffForBase(project, base) {
  if (base === '--staged') return gitUtils.getStagedDiff(project);
  if (base === '--uncommitted' || base === 'HEAD') return gitUtils.getDiffVsRef(project, 'HEAD');
  if (base === '--vs-main') {
    const def = await gitUtils.getDefaultReviewBase(project);
    return gitUtils.getDiffVsRef(project, def.kind === 'vs-main' ? def.mergeBase : 'HEAD');
  }
  return gitUtils.getFullDiff(project, base || undefined);
}
async function diffStatForBase(project, base) {
  if (base === '--uncommitted' || base === 'HEAD') return gitUtils.getDiffStatVsRef(project, 'HEAD');
  if (base === '--vs-main') {
    const def = await gitUtils.getDefaultReviewBase(project);
    return gitUtils.getDiffStatVsRef(project, def.kind === 'vs-main' ? def.mergeBase : 'HEAD');
  }
  return gitUtils.getDiffStat(project, base || undefined);
}

// --- Route Handler ---
function handleReviewApi(req, res, url) {
  const p = url.pathname;
  const m = req.method;

  // GET /api/reviews/document?path=...&line=...
  if (p === '/api/reviews/document' && m === 'GET') {
    try {
      const filePath = url.searchParams.get('path');
      const line = url.searchParams.get('line') || 1;
      const document = documentReview.readDocument(filePath, { line, cwd: url.searchParams.get('cwd') || '' });
      return jsonResponse(res, 200, { document });
    } catch (e) {
      return jsonResponse(res, e.statusCode || 500, reviewErrorPayload(e));
    }
  }

  // POST /api/reviews/document-review - create/reuse a review record for one document
  if (p === '/api/reviews/document-review' && m === 'POST') {
    readBody(req).then(body => {
      const document = documentReview.readDocument(body.path, {
        line: body.line || 1,
        cwd: body.cwd || '',
      });
      const review = db.listReviews({
        project_path: document.projectRoot,
        review_type: 'doc',
        artifact_path: document.path,
        limit: 1,
      })[0];
      const id = review?.id || db.createReview({
        session_id: body.session_id || null,
        project_path: document.projectRoot,
        review_type: 'doc',
        artifact_path: document.path,
        artifact_hash: document.hash,
        base_ref: `doc:${document.relativePath}`,
      });
      db.updateReview(id, {
        file_count: 1,
        artifact_hash: document.hash,
        summary: document.headings[0]?.text || path.basename(document.path),
      });
      return jsonResponse(res, review ? 200 : 201, {
        id,
        review: db.getReview(id),
        document,
      });
    }).catch(e => jsonResponse(res, e.statusCode || 400, reviewErrorPayload(e)));
    return true;
  }

  // GET /api/reviews/diff?project=...&base=...
  if (p === '/api/reviews/diff' && m === 'GET') {
    const project = url.searchParams.get('project');
    const base = url.searchParams.get('base') || '';
    if (!project) return jsonResponse(res, 400, { error: 'project required' });
    if (!isValidProjectPath(project)) return jsonResponse(res, 403, { error: 'Invalid project path' });
    diffForBase(project, base)
      .then(files => jsonResponse(res, 200, { files }))
      .catch(e => jsonResponse(res, 500, { error: e.message }));
    return true;
  }

  // GET /api/reviews/diff-stat?project=...&base=...
  if (p === '/api/reviews/diff-stat' && m === 'GET') {
    const project = url.searchParams.get('project');
    if (!project) return jsonResponse(res, 400, { error: 'project required' });
    if (!isValidProjectPath(project)) return jsonResponse(res, 403, { error: 'Invalid project path' });
    const base = url.searchParams.get('base') || '';
    diffStatForBase(project, base)
      .then(files => jsonResponse(res, 200, { files }))
      .catch(e => jsonResponse(res, 500, { error: e.message }));
    return true;
  }

  // GET /api/reviews/base-info?project=... — the default review base + per-base file counts,
  // so the client can default the COMPARE selector to "all branch work vs main" and label
  // each option with its count.
  if (p === '/api/reviews/base-info' && m === 'GET') {
    const project = url.searchParams.get('project');
    if (!project) return jsonResponse(res, 400, { error: 'project required' });
    if (!isValidProjectPath(project)) return jsonResponse(res, 403, { error: 'Invalid project path' });
    gitUtils.getDefaultReviewBase(project).then(async (def) => {
      const [vsMain, uncommitted, working, staged] = await Promise.all([
        def.mergeBase ? gitUtils.getDiffStatVsRef(project, def.mergeBase).catch(() => []) : Promise.resolve([]),
        gitUtils.getDiffStatVsRef(project, 'HEAD').catch(() => []),
        gitUtils.getDiffStat(project).catch(() => []),
        gitUtils.getDiffStat(project, '--staged').catch(() => []),
      ]);
      jsonResponse(res, 200, {
        defaultBase: def.sentinel,
        kind: def.kind,
        mainBranch: def.mainBranch,
        mergeBase: def.mergeBase,
        counts: {
          'vs-main': vsMain.length,
          'uncommitted': uncommitted.length,
          'working': working.length,
          'staged': staged.length,
        },
      });
    }).catch(e => jsonResponse(res, 500, { error: e.message }));
    return true;
  }

  // GET /api/reviews/file-lines?project=&path=&from=&to=&ref= — fetch the "new"-side file
  // lines in [from,to] so the diff view can expand context hidden between hunks.
  if (p === '/api/reviews/file-lines' && m === 'GET') {
    const project = url.searchParams.get('project');
    const relPath = url.searchParams.get('path');
    if (!project || !relPath) return jsonResponse(res, 400, { error: 'project and path required' });
    if (!isValidProjectPath(project)) return jsonResponse(res, 403, { error: 'Invalid project path' });
    const from = Math.max(1, parseInt(url.searchParams.get('from') || '1', 10));
    const toRaw = parseInt(url.searchParams.get('to') || '0', 10);
    const ref = url.searchParams.get('ref') || '';
    gitUtils.getFileAtRef(project, relPath, ref).then(content => {
      const all = String(content).split('\n');
      const to = toRaw > 0 ? Math.min(toRaw, all.length) : all.length;
      const lines = all.slice(from - 1, to).map((text, i) => ({ num: from + i, text }));
      jsonResponse(res, 200, { lines, total: all.length });
    }).catch(e => jsonResponse(res, 500, { error: e.message }));
    return true;
  }

  // GET /api/reviews/commits?project=...
  if (p === '/api/reviews/commits' && m === 'GET') {
    const project = url.searchParams.get('project');
    if (!project) return jsonResponse(res, 400, { error: 'project required' });
    if (!isValidProjectPath(project)) return jsonResponse(res, 403, { error: 'Invalid project path' });
    gitUtils.getCommits(project, 30)
      .then(commits => jsonResponse(res, 200, { commits }))
      .catch(e => jsonResponse(res, 500, { error: e.message }));
    return true;
  }

  // GET /api/reviews/commit-log?project=...&count=...
  if (p === '/api/reviews/commit-log' && m === 'GET') {
    const project = url.searchParams.get('project');
    const count = parseInt(url.searchParams.get('count') || '20', 10);
    if (!project) return jsonResponse(res, 400, { error: 'project required' });
    if (!isValidProjectPath(project)) return jsonResponse(res, 403, { error: 'Invalid project path' });
    gitUtils.getCommitLog(project, Math.min(count, 50))
      .then(commits => jsonResponse(res, 200, { commits }))
      .catch(e => jsonResponse(res, 500, { error: e.message }));
    return true;
  }

  // GET /api/reviews/branch?project=...
  if (p === '/api/reviews/branch' && m === 'GET') {
    const project = url.searchParams.get('project');
    if (!project) return jsonResponse(res, 400, { error: 'project required' });
    if (!isValidProjectPath(project)) return jsonResponse(res, 403, { error: 'Invalid project path' });
    gitUtils.getBranch(project)
      .then(branch => jsonResponse(res, 200, { branch }))
      .catch(e => jsonResponse(res, 500, { error: e.message }));
    return true;
  }

  // GET /api/reviews/tracked-projects - list all tracked project folders with change counts
  if (p === '/api/reviews/tracked-projects' && m === 'GET') {
    const getTrackedProjects = module.exports._getTrackedProjects;
    if (!getTrackedProjects) return jsonResponse(res, 500, { error: 'tracker not initialized' });
    getTrackedProjects()
      .then(result => {
        if (Array.isArray(result)) return jsonResponse(res, 200, { projects: result });
        const projects = Array.isArray(result && result.projects) ? result.projects : [];
        jsonResponse(res, 200, { projects, scan: result && result.scan ? result.scan : null });
      })
      .catch(e => jsonResponse(res, 500, { error: e.message }));
    return true;
  }

  // GET /api/reviews/changes?project=... - badge polling endpoint
  if (p === '/api/reviews/changes' && m === 'GET') {
    const project = url.searchParams.get('project');
    if (!project) return jsonResponse(res, 400, { error: 'project required' });
    if (!isValidProjectPath(project)) return jsonResponse(res, 403, { error: 'Invalid project path' });
    checkForChanges(project)
      .then(result => jsonResponse(res, 200, result))
      .catch(e => jsonResponse(res, 500, { error: e.message }));
    return true;
  }

  // GET /api/reviews/file?project=...&path=... - serve image files from project
  if (p === '/api/reviews/file' && m === 'GET') {
    const project = url.searchParams.get('project');
    const filePath = url.searchParams.get('path');
    if (!project || !filePath) return jsonResponse(res, 400, { error: 'project and path required' });
    if (!isValidProjectPath(project)) return jsonResponse(res, 403, { error: 'Invalid project path' });
    // Only allow image extensions
    const ext = path.extname(filePath).toLowerCase();
    const imageExts = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.ico': 'image/x-icon', '.bmp': 'image/bmp' };
    if (!imageExts[ext]) return jsonResponse(res, 400, { error: 'Only image files are supported' });
    // Prevent path traversal
    const resolved = path.resolve(project, filePath);
    if (!resolved.startsWith(path.resolve(project) + path.sep)) return jsonResponse(res, 403, { error: 'Invalid file path' });
    try {
      const data = fs.readFileSync(resolved);
      res.writeHead(200, { 'Content-Type': imageExts[ext], 'Content-Length': data.length, 'Cache-Control': 'no-cache' });
      res.end(data);
    } catch (e) {
      res.writeHead(404, { 'Content-Type': 'text/plain' });
      res.end('File not found');
    }
    return true;
  }

  // --- Review CRUD ---

  // POST /api/reviews - create a new review
  if (p === '/api/reviews' && m === 'POST') {
    readBody(req).then(body => {
      const { session_id, project_path, base_ref } = body;
      if (!project_path) return jsonResponse(res, 400, { error: 'project_path required' });
      if (!isValidProjectPath(project_path)) return jsonResponse(res, 403, { error: 'Invalid project path' });
      const id = db.createReview({ session_id, project_path, base_ref });
      jsonResponse(res, 201, { id });
    }).catch(e => jsonResponse(res, 400, { error: e.message }));
    return true;
  }

  // GET /api/reviews - list reviews
  if (p === '/api/reviews' && m === 'GET') {
    const project_path = url.searchParams.get('project');
    const session_id = url.searchParams.get('session');
    const reviews = db.listReviews({ project_path, session_id });
    return jsonResponse(res, 200, reviews);
  }

  // GET /api/reviews/:id
  const reviewMatch = p.match(/^\/api\/reviews\/(\d+)$/);
  if (reviewMatch && m === 'GET') {
    const review = db.getReview(parseInt(reviewMatch[1]));
    if (!review) return jsonResponse(res, 404, { error: 'Review not found' });
    return jsonResponse(res, 200, review);
  }

  // PUT /api/reviews/:id
  if (reviewMatch && m === 'PUT') {
    readBody(req).then(body => {
      db.updateReview(parseInt(reviewMatch[1]), body);
      jsonResponse(res, 200, { ok: true });
    }).catch(e => jsonResponse(res, 400, { error: e.message }));
    return true;
  }

  // DELETE /api/reviews/:id
  if (reviewMatch && m === 'DELETE') {
    db.deleteReview(parseInt(reviewMatch[1]));
    return jsonResponse(res, 200, { ok: true });
  }

  // --- Review Comments ---

  // POST /api/reviews/:id/comments
  const commentsMatch = p.match(/^\/api\/reviews\/(\d+)\/comments$/);
  if (commentsMatch && m === 'POST') {
    readBody(req).then(body => {
      const reviewId = parseInt(commentsMatch[1], 10);
      if (!db.getReview(reviewId)) return jsonResponse(res, 404, { error: 'Review not found' });
      const id = db.addReviewComment(sanitizeReviewCommentInput(reviewId, body));
      jsonResponse(res, 201, { id });
    }).catch(e => jsonResponse(res, 400, { error: e.message }));
    return true;
  }

  // PUT /api/review-comments/:id
  const commentMatch = p.match(/^\/api\/review-comments\/(\d+)$/);
  if (commentMatch && m === 'PUT') {
    readBody(req).then(body => {
      db.updateReviewComment(parseInt(commentMatch[1], 10), sanitizeReviewCommentUpdate(body));
      jsonResponse(res, 200, { ok: true });
    }).catch(e => jsonResponse(res, 400, { error: e.message }));
    return true;
  }

  // DELETE /api/review-comments/:id
  if (commentMatch && m === 'DELETE') {
    db.deleteReviewComment(parseInt(commentMatch[1]));
    return jsonResponse(res, 200, { ok: true });
  }

  // --- Compose review prompt for sending to Claude ---
  const composeMatch = p.match(/^\/api\/reviews\/(\d+)\/compose$/);
  if (composeMatch && m === 'GET') {
    const review = db.getReview(parseInt(composeMatch[1]));
    if (!review) return jsonResponse(res, 404, { error: 'Review not found' });
    const prompt = composeReviewPrompt(review);
    return jsonResponse(res, 200, { prompt });
  }

  return false; // Not handled
}

// Compose structured review prompt from comments
function composeReviewPrompt(review) {
  const comments = review.comments.filter(c => c.status === 'open');
  if (comments.length === 0) return 'No review comments to address.';

  const byFile = {};
  for (const c of comments) {
    if (!byFile[c.file_path]) byFile[c.file_path] = [];
    byFile[c.file_path].push(c);
  }

  const reviewKind = review.review_type === 'doc' ? 'documentation review' : 'code review';
  const lines = [`Please address these ${reviewKind} comments:\n`];

  for (const [filePath, fileComments] of Object.entries(byFile)) {
    lines.push(`## ${filePath}\n`);
    for (const c of fileComments) {
      const lineRef = c.line_end && c.line_end !== c.line_start
        ? `Lines ${c.line_start}-${c.line_end}`
        : `Line ${c.line_start}`;
      const anchor = c.anchor_kind === 'block' && c.block_id ? ` (${c.block_id})` : '';
      const normalizedSeverity = normalizeReviewSeverity(c.severity);
      const severity = normalizedSeverity !== 'comment' ? ` [${normalizedSeverity}]` : '';
      lines.push(`**${lineRef}${anchor}**${severity}: ${c.body}\n`);
    }
  }

  if (review.summary) {
    lines.push(`\n## Overall\n\n${review.summary}`);
  }

  return lines.join('\n');
}

module.exports = {
  handleReviewApi,
  checkForChanges,
  composeReviewPrompt,
  _normalizeReviewSeverity: normalizeReviewSeverity,
  _normalizeReviewStatus: normalizeReviewStatus,
  _sanitizeReviewCommentInput: sanitizeReviewCommentInput,
  _sanitizeReviewCommentUpdate: sanitizeReviewCommentUpdate,
  _documentReview: documentReview,
};
