'use strict';

const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');

const SUPPORTED_EXTENSIONS = new Set(['.md', '.markdown', '.mdown', '.txt']);
const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;

function httpError(message, statusCode, code) {
  const err = new Error(message);
  err.statusCode = statusCode;
  err.code = code || 'ERR_DOCUMENT_REVIEW';
  return err;
}

function normalizeLine(value, fallback = 1) {
  const n = Number(value);
  return Number.isInteger(n) && n > 0 ? n : fallback;
}

function allowedRoots(options = {}) {
  const roots = [];
  const home = options.home || process.env.HOME || os.homedir();
  if (home) roots.push(home);
  if (Array.isArray(options.roots)) roots.push(...options.roots);
  return roots
    .filter(Boolean)
    .map((root) => {
      try { return fs.realpathSync.native ? fs.realpathSync.native(root) : fs.realpathSync(root); }
      catch { return path.resolve(root); }
    });
}

function isInside(root, target) {
  return target === root || target.startsWith(root + path.sep);
}

function splitPathAndLine(input, fallbackLine = 1) {
  let raw = String(input || '').trim();
  let line = fallbackLine;
  const match = raw.match(/^(.*):(\d+)$/);
  if (match) {
    raw = match[1];
    line = normalizeLine(match[2], fallbackLine);
  }
  return { rawPath: raw, line };
}

function realpathOrResolve(value) {
  const resolved = path.resolve(value);
  try { return fs.realpathSync.native ? fs.realpathSync.native(resolved) : fs.realpathSync(resolved); }
  catch { return resolved; }
}

function normalizeBaseCwd(cwd) {
  if (!cwd) return '';
  const raw = String(cwd || '').trim();
  if (!raw || !path.isAbsolute(raw)) return '';
  try {
    const stat = fs.statSync(raw);
    const dir = stat.isDirectory() ? raw : path.dirname(raw);
    return realpathOrResolve(dir);
  } catch {
    return '';
  }
}

function resolveInputPath(rawPath, options = {}) {
  if (!rawPath) return '';
  if (/^~(?:\/|$)/.test(rawPath)) {
    const home = options.home || process.env.HOME || os.homedir();
    if (!home) return '';
    return path.resolve(home, rawPath === '~' ? '' : rawPath.slice(2));
  }
  if (path.isAbsolute(rawPath)) return path.resolve(rawPath);
  const baseCwd = normalizeBaseCwd(options.cwd);
  if (!baseCwd) throw httpError('relative document path requires a session cwd', 400, 'EINVAL');
  return path.resolve(baseCwd, rawPath);
}

function projectRootFor(filePath) {
  const cwd = fs.statSync(filePath).isDirectory() ? filePath : path.dirname(filePath);
  try {
    return execFileSync('git', ['rev-parse', '--show-toplevel'], {
      cwd,
      encoding: 'utf8',
      stdio: ['ignore', 'pipe', 'ignore'],
    }).trim();
  } catch {
    return '';
  }
}

function reviewRootFor(filePath, options = {}) {
  const gitRoot = projectRootFor(filePath);
  if (gitRoot) return gitRoot;

  const realPath = realpathOrResolve(filePath);
  const baseCwd = normalizeBaseCwd(options.cwd);
  if (baseCwd && isInside(baseCwd, realPath)) return baseCwd;
  return realpathOrResolve(path.dirname(realPath));
}

function normalizeRelativeDocRef(rawPath) {
  let value = String(rawPath || '').trim().replace(/\\/g, '/');
  while (value.startsWith('./')) value = value.slice(2);
  return value.replace(/^\/+/, '');
}

function trackedDocumentPaths(projectRoot, options = {}) {
  const maxPaths = Math.max(1, Number(options.maxSearchPaths) || 10000);
  let output = '';
  try {
    output = execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], {
      cwd: projectRoot,
      encoding: 'utf8',
      stdio: ['ignore', 'pipe', 'ignore'],
      maxBuffer: Math.max(1024 * 1024, Number(options.maxSearchBytes) || 8 * 1024 * 1024),
    });
  } catch {
    return [];
  }
  const paths = output.split('\0').filter(Boolean);
  const docPaths = paths.filter((entry) => SUPPORTED_EXTENSIONS.has(path.extname(entry).toLowerCase()));
  if (docPaths.length > maxPaths) return [];
  return docPaths;
}

function projectDocumentCandidates(rawPath, baseCwd, options = {}) {
  const normalized = normalizeRelativeDocRef(rawPath);
  if (!normalized || path.isAbsolute(rawPath) || /^~(?:\/|$)/.test(rawPath)) return [];
  if (!SUPPORTED_EXTENSIONS.has(path.extname(normalized).toLowerCase())) return [];
  const projectRoot = projectRootFor(baseCwd);
  if (!projectRoot) return [];
  let realProjectRoot = '';
  try {
    realProjectRoot = fs.realpathSync.native ? fs.realpathSync.native(projectRoot) : fs.realpathSync(projectRoot);
  } catch {
    realProjectRoot = path.resolve(projectRoot);
  }
  const roots = allowedRoots(options);
  const seen = new Set();
  return trackedDocumentPaths(projectRoot, options)
    .filter((entry) => entry === normalized || entry.endsWith('/' + normalized))
    .filter((entry) => {
      if (seen.has(entry)) return false;
      seen.add(entry);
      return true;
    })
    .map((entry) => {
      const candidatePath = path.join(projectRoot, entry);
      try {
        const stat = fs.statSync(candidatePath);
        if (!stat.isFile()) return null;
        const realPath = fs.realpathSync.native ? fs.realpathSync.native(candidatePath) : fs.realpathSync(candidatePath);
        if (!isInside(realProjectRoot, realPath)) return null;
        if (!roots.some((root) => isInside(root, realPath))) return null;
        return {
          path: realPath,
          relativePath: path.relative(realProjectRoot, realPath),
          projectRoot: realProjectRoot,
        };
      } catch {
        return null;
      }
    })
    .filter(Boolean);
}

function resolveUniqueProjectDocument(rawPath, baseCwd, options = {}) {
  const candidates = projectDocumentCandidates(rawPath, baseCwd, options);
  if (candidates.length === 1) return candidates[0].path;
  if (candidates.length > 1) {
    const err = httpError(
      `multiple documents match ${String(rawPath || '').trim()}`,
      409,
      'EAMBIGUOUS'
    );
    err.candidates = candidates.slice(0, 50);
    throw err;
  }
  return '';
}

function resolveDocumentTarget(input, options = {}) {
  const { rawPath, line } = splitPathAndLine(input, options.line || 1);
  if (!rawPath) throw httpError('path required', 400, 'EINVAL');

  let resolved = resolveInputPath(rawPath, options);
  let realPath = '';
  try {
    const stat = fs.statSync(resolved);
    if (!stat.isFile()) throw httpError('document path must be a file', 400, 'EINVAL');
    if (stat.size > (options.maxBytes || DEFAULT_MAX_BYTES)) {
      throw httpError('document too large for review', 413, 'ETOOLARGE');
    }
    realPath = fs.realpathSync.native ? fs.realpathSync.native(resolved) : fs.realpathSync(resolved);
  } catch (err) {
    if (err.statusCode) throw err;
    const baseCwd = normalizeBaseCwd(options.cwd);
    const found = baseCwd ? resolveUniqueProjectDocument(rawPath, baseCwd, options) : '';
    if (found) {
      resolved = found;
      const stat = fs.statSync(resolved);
      if (!stat.isFile()) throw httpError('document path must be a file', 400, 'EINVAL');
      if (stat.size > (options.maxBytes || DEFAULT_MAX_BYTES)) {
        throw httpError('document too large for review', 413, 'ETOOLARGE');
      }
      realPath = fs.realpathSync.native ? fs.realpathSync.native(resolved) : fs.realpathSync(resolved);
    } else {
      throw httpError('document not found', 404, 'ENOENT');
    }
  }

  const roots = allowedRoots(options);
  if (!roots.some((root) => isInside(root, realPath))) {
    throw httpError('document must be under an allowed root', 403, 'EACCES');
  }

  const ext = path.extname(realPath).toLowerCase();
  if (!SUPPORTED_EXTENSIONS.has(ext)) {
    throw httpError('unsupported document type', 400, 'EINVAL');
  }

  const projectRoot = reviewRootFor(realPath, options);
  const realProjectRoot = fs.realpathSync.native ? fs.realpathSync.native(projectRoot) : fs.realpathSync(projectRoot);
  if (!isInside(realProjectRoot, realPath)) {
    throw httpError('document is outside the review root', 403, 'EACCES');
  }

  return {
    path: realPath,
    line,
    projectRoot: realProjectRoot,
    relativePath: path.relative(realProjectRoot, realPath),
  };
}

function slugify(value) {
  return String(value || '')
    .toLowerCase()
    .replace(/[`*_~[\]()]/g, '')
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    || 'section';
}

function uniqueSlug(base, seen) {
  const root = slugify(base);
  const count = seen.get(root) || 0;
  seen.set(root, count + 1);
  return count ? `${root}-${count + 1}` : root;
}

function blockId(type, start, end, text) {
  const hash = crypto.createHash('sha1').update(`${type}:${start}:${end}:${text}`).digest('hex').slice(0, 8);
  return `b-${start}-${end}-${hash}`;
}

function parseDocument(content) {
  const lines = content.split(/\r?\n/);
  const headings = [];
  const blocks = [];
  const headingSlugs = new Map();
  let i = 0;

  const pushBlock = (type, start, end) => {
    const text = lines.slice(start - 1, end).join('\n').trim();
    if (!text) return;
    blocks.push({
      id: blockId(type, start, end, text),
      type,
      lineStart: start,
      lineEnd: end,
      text,
    });
  };

  while (i < lines.length) {
    const line = lines[i];
    const lineNo = i + 1;
    if (!line.trim()) {
      i += 1;
      continue;
    }

    const heading = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
    if (heading) {
      const text = heading[2].trim();
      const slug = uniqueSlug(text, headingSlugs);
      headings.push({ level: heading[1].length, text, line: lineNo, slug });
      blocks.push({
        id: `h-${lineNo}-${slug}`,
        type: 'heading',
        level: heading[1].length,
        lineStart: lineNo,
        lineEnd: lineNo,
        text,
        slug,
      });
      i += 1;
      continue;
    }

    if (/^```/.test(line.trim())) {
      const start = lineNo;
      i += 1;
      while (i < lines.length && !/^```/.test(lines[i].trim())) i += 1;
      if (i < lines.length) i += 1;
      pushBlock('code', start, i);
      continue;
    }

    if (/^\s*(?:[-*+]\s+|\d+\.\s+)/.test(line)) {
      const start = lineNo;
      i += 1;
      while (i < lines.length && (/^\s*(?:[-*+]\s+|\d+\.\s+)/.test(lines[i]) || /^\s{2,}\S/.test(lines[i]))) i += 1;
      pushBlock('list', start, i);
      continue;
    }

    const start = lineNo;
    i += 1;
    while (
      i < lines.length &&
      lines[i].trim() &&
      !/^(#{1,6})\s+/.test(lines[i]) &&
      !/^```/.test(lines[i].trim()) &&
      !/^\s*(?:[-*+]\s+|\d+\.\s+)/.test(lines[i])
    ) {
      i += 1;
    }
    pushBlock('paragraph', start, i);
  }

  return {
    lines: lines.map((text, index) => ({ number: index + 1, text })),
    headings,
    blocks,
  };
}

function readDocument(input, options = {}) {
  const target = resolveDocumentTarget(input, options);
  const content = fs.readFileSync(target.path, 'utf8');
  const parsed = parseDocument(content);
  const hash = crypto.createHash('sha256').update(content).digest('hex');
  return {
    path: target.path,
    projectRoot: target.projectRoot,
    relativePath: target.relativePath,
    line: Math.min(target.line, Math.max(parsed.lines.length, 1)),
    language: path.extname(target.path).toLowerCase().replace(/^\./, '') || 'text',
    hash,
    size: Buffer.byteLength(content),
    content,
    ...parsed,
  };
}

function parseDocumentPathname(pathname, options = {}) {
  let decoded = '';
  try { decoded = decodeURIComponent(String(pathname || '')); } catch { return null; }
  const match = decoded.match(/^(\/.*\.(?:md|markdown|mdown|txt))(?::(\d+))?$/i);
  if (!match) return null;
  try {
    const target = resolveDocumentTarget(match[1], { ...options, line: normalizeLine(match[2], 1) });
    return { path: target.path, line: target.line };
  } catch {
    return null;
  }
}

module.exports = {
  readDocument,
  parseDocument,
  parseDocumentPathname,
  resolveDocumentTarget,
  splitPathAndLine,
  projectDocumentCandidates,
  resolveUniqueProjectDocument,
};
