// VENDORED from app/lib/claude-code/optimal-files/merge.js and
// app/lib/claude-code/optimal-files/bundle.js.
//
// The CLI is a sibling package and can't import from app/lib/ without
// monorepo tooling, but Phase 6's `dashclaw code apply` needs the
// path-traversal guard and the markdown merge applier locally. Keep this
// file in sync with the canonical source via
// `node scripts/sync-cli-vendored-code.mjs`.
//
// Source of truth:
//   app/lib/claude-code/optimal-files/merge.js
//   app/lib/claude-code/optimal-files/bundle.js#absolutize
//
// Only `applyMerge`, `previewMerge`, the heading/bullet helpers, and the
// `_ensureInsideProject` guard are vendored. Everything else stays on the
// server side.

import path from 'node:path';

// --- _ensureInsideProject (originally from bundle.js#absolutize) ---------

export function _ensureInsideProject(projectCwd, candidatePath) {
  if (!projectCwd || !candidatePath) return null;
  let abs;
  if (path.isAbsolute(candidatePath)) {
    abs = path.normalize(candidatePath);
  } else {
    abs = path.normalize(path.join(projectCwd, candidatePath));
  }
  const cwdNorm = path.normalize(projectCwd);
  const ci = process.platform === 'win32';
  const a = ci ? abs.toLowerCase() : abs;
  const b = ci ? cwdNorm.toLowerCase() : cwdNorm;
  if (a !== b && !a.startsWith(b + path.sep)) return null;
  return abs;
}

// --- markdown merge (originally from merge.js) ---------------------------

export function parseMarkdownSections(text) {
  const lines = String(text || '').split(/\r?\n/);
  const sections = [];
  let cur = { level: 0, heading: '__preamble__', headingRaw: '', body: [] };
  for (const line of lines) {
    const m = line.match(/^(#{1,6})\s+(.+?)\s*$/);
    if (m) {
      sections.push(cur);
      cur = { level: m[1].length, heading: normalizeHeading(m[2]), headingRaw: m[2], body: [] };
    } else {
      cur.body.push(line);
    }
  }
  sections.push(cur);
  return sections;
}

export function normalizeHeading(h) {
  return String(h || '')
    .toLowerCase()
    .replace(/[`*_]/g, '')
    .replace(/[^\w\s]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

function parseBullets(bodyLines) {
  const bullets = [];
  const other = [];
  for (const line of bodyLines) {
    if (/^\s*[-*]\s+/.test(line)) bullets.push({ raw: line, normalized: normalizeBullet(line) });
    else other.push(line);
  }
  return { bullets, other };
}

function normalizeBullet(line) {
  return String(line || '')
    .replace(/^\s*[-*]\s+/, '')
    .replace(/`[^`]*`/g, ' ')
    .replace(/[^a-z0-9\s]+/gi, ' ')
    .toLowerCase()
    .trim()
    .split(/\s+/)
    .slice(0, 8)
    .join(' ');
}

function bulletAlreadyPresent(generatedNorm, existingBullets) {
  if (!generatedNorm) return true;
  for (const b of existingBullets) {
    if (!b.normalized) continue;
    if (b.normalized.startsWith(generatedNorm)) return true;
    if (generatedNorm.startsWith(b.normalized) && b.normalized.length >= 4) return true;
    const aWords = generatedNorm.split(' ');
    const bWords = b.normalized.split(' ');
    let shared = 0;
    for (let i = 0; i < Math.min(aWords.length, bWords.length); i++) {
      if (aWords[i] === bWords[i]) shared++; else break;
    }
    if (shared >= 5) return true;
  }
  return false;
}

function isFooterSection(section) {
  if (!section || !Array.isArray(section.body)) return false;
  const joined = section.body.join('\n').toLowerCase();
  return (joined.includes('generated by dashclaw') || joined.includes('generated by agentlens')) && joined.includes('review before committing');
}

function renderSection(section) {
  return `## ${section.headingRaw}\n${section.body.join('\n')}`.replace(/\n+$/, '\n');
}

export function previewMerge(existing, generated) {
  const ex = parseMarkdownSections(existing);
  const gn = parseMarkdownSections(generated);
  const exByHeading = new Map();
  for (const s of ex) {
    if (s.heading === '__preamble__') continue;
    if (!exByHeading.has(s.heading)) exByHeading.set(s.heading, s);
  }
  const appendSections = [];
  const sharedSections = [];
  for (let i = 0; i < gn.length; i++) {
    const s = gn[i];
    if (s.heading === '__preamble__') continue;
    if (isFooterSection(s)) continue;
    if (s.level !== 2) continue;
    const exSection = exByHeading.get(s.heading);
    if (!exSection) {
      appendSections.push({ heading: s.heading, headingRaw: s.headingRaw, bodyText: renderSection(s) });
    } else {
      const exBullets = parseBullets(exSection.body).bullets;
      const genBullets = parseBullets(s.body).bullets;
      const candidates = [];
      for (const gb of genBullets) {
        if (gb.normalized && !bulletAlreadyPresent(gb.normalized, exBullets)) {
          candidates.push({ text: gb.raw.replace(/^\s*[-*]\s+/, '- ') });
        }
      }
      if (candidates.length) {
        sharedSections.push({
          heading: s.heading,
          headingRaw: s.headingRaw,
          existingBulletCount: exBullets.length,
          candidateBullets: candidates,
        });
      }
    }
  }
  return {
    appendSections,
    sharedSections,
    newSectionCount: appendSections.length,
    sharedSectionCount: sharedSections.length,
    candidateBulletCount: sharedSections.reduce((acc, s) => acc + s.candidateBullets.length, 0),
  };
}

function trimTrailingBlanks(lines) {
  const out = lines.slice();
  while (out.length && out[out.length - 1].trim() === '') out.pop();
  return out;
}

export function applyMerge(existing, generated, selection) {
  const preview = previewMerge(existing, generated);
  selection = selection || {};
  const acceptedHeadings = new Set((selection.acceptedHeadings || []).map(h => normalizeHeading(h)));
  const acceptedBulletsByHeading = new Map();
  for (const b of (selection.acceptedBullets || [])) {
    const k = normalizeHeading(b.heading);
    if (!acceptedBulletsByHeading.has(k)) acceptedBulletsByHeading.set(k, []);
    acceptedBulletsByHeading.get(k).push(b.text);
  }
  let merged = String(existing == null ? '' : existing);
  if (merged && !merged.endsWith('\n')) merged += '\n';
  if (acceptedBulletsByHeading.size) {
    const sections = parseMarkdownSections(merged);
    const rebuilt = [];
    for (const s of sections) {
      if (s.heading === '__preamble__') { rebuilt.push(s.body.join('\n')); continue; }
      const headingLine = `${'#'.repeat(s.level)} ${s.headingRaw}`;
      const accepted = acceptedBulletsByHeading.get(s.heading) || [];
      if (accepted.length) {
        const trimmedBody = trimTrailingBlanks(s.body);
        rebuilt.push(headingLine);
        rebuilt.push(trimmedBody.join('\n'));
        rebuilt.push('');
        rebuilt.push('<!-- dashclaw:merged-bullets -->');
        for (const t of accepted) rebuilt.push(t.startsWith('- ') ? t : `- ${t}`);
        rebuilt.push('<!-- /dashclaw:merged-bullets -->');
        rebuilt.push('');
      } else {
        rebuilt.push(headingLine);
        rebuilt.push(s.body.join('\n'));
      }
    }
    merged = rebuilt.join('\n').replace(/\n+$/, '\n');
  }
  const wholeAdditions = preview.appendSections.filter(s => acceptedHeadings.has(s.heading));
  if (wholeAdditions.length) {
    if (!merged.endsWith('\n')) merged += '\n';
    merged += '\n<!-- dashclaw:merged-sections -->\n\n';
    for (const s of wholeAdditions) {
      merged += s.bodyText.endsWith('\n') ? s.bodyText : s.bodyText + '\n';
      merged += '\n';
    }
    merged += '<!-- /dashclaw:merged-sections -->\n';
  }
  return {
    merged,
    additions: {
      sections: wholeAdditions.map(s => s.headingRaw),
      bulletCount: [...acceptedBulletsByHeading.values()].reduce((acc, l) => acc + l.length, 0),
    },
  };
}
