/*
 * #145: source-to-concept mapping engine, provenance extraction, reference-path
 * derivation, and link rewriting.
 *
 * Sibling of `migration.js` rather than an extension of it (SRP): `migration.js`
 * owns plan orchestration -- dispositions, questions, applying answers; this module
 * owns the mapping *rules* that orchestration consumes -- what type a source with
 * no explicit `type` deterministically is, what bundle-relative concept path a type
 * maps a source to, what provenance a source's own frontmatter already carries
 * verbatim, and how a migrating body's own Markdown links are rewritten when the
 * mapping is unambiguous. `migration.js` calls this module; this module never
 * calls back.
 *
 * #177 (#157): there is deliberately no residue target-path rule here. Migration
 * residue is report-only -- an unsupported source stays exactly where it is and
 * nothing is copied -- so no `references/<path>` mirror is derived for it.
 *
 * Binding rules from #131 this module enforces:
 *   - an ambiguous type is never guessed: every rule below is evidence -- a
 *     conventional directory name, a conventional filename, or a structural
 *     template signature -- never a judgement about a document's prose meaning.
 *     A source this module cannot place stays undetermined; #144's own question
 *     round is the only path from there to a type, never a guess here. There is
 *     deliberately no generic `Note` fallback: a source with no evidence returns
 *     `null`, full stop.
 *   - never fabricate `sources`, `generated`, `verified`, actors, or freshness:
 *     `extractProvenance` returns exactly what a source's own frontmatter already
 *     declares, or `null`. It manufactures nothing and repairs nothing.
 *   - rewrite only parsed standard Markdown links, only when the mapping is
 *     unambiguous, and never inside fenced or inline code -- reusing
 *     `validation.js`'s own `withoutFencedCode` and `bodyLinkPath` rather than a
 *     second link parser.
 */

const path = require('node:path');
const validation = require('./validation');

// ------------------------------------------------------- type inference (deterministic)

function dirSegments(sourcePath) {
  const dir = path.posix.dirname(sourcePath);
  return dir === '.' ? [] : dir.toLowerCase().split('/');
}

const ADR_FILENAME = /^adr[-_]?\d+/i;
const RELEASE_FILENAME = /^v?\d+\.\d+\.\d+\.md$/i;

// Michael Nygard's ADR template's own four headings, exactly as documented in
// #130's research (`docs/spec` migration mapping). All four, as their own heading
// line, is a structural fingerprint of that specific template -- not a guess about
// what the prose under them means.
const ADR_HEADINGS = ['Status', 'Context', 'Decision', 'Consequences'];

function hasHeading(strippedBody, text) {
  return new RegExp(`^#{1,6}[ \\t]+${text}[ \\t]*$`, 'mi').test(strippedBody);
}

function isAdrTemplate(strippedBody) {
  return ADR_HEADINGS.every((heading) => hasHeading(strippedBody, heading));
}

function hasRuntimeField(tree) {
  return Boolean(tree) && tree.runtime !== undefined && tree.runtime !== null && tree.runtime !== '';
}

// Ordered, evidence-only rules. First match wins; the evidence classes below do
// not overlap in practice, and none of them reads or judges prose meaning -- a
// conventional directory name, a conventional filename, or Nygard's ADR heading
// template, nothing else. A source matching none of these returns `null`: #144's
// own question round is the only legitimate path from there to a type (never a
// guess here, never a generic `Note` fallback).
//
// #179 (#158): `Glossary` no longer has a content rule. Inferring it from two or
// more `**Label**: value` lines read ordinary emphasis-labelled prose as a term
// list -- `**Source**:`, `**Implication**:`, `**Prerequisites**:` in a research
// report all matched -- and a `migrate` entry carries no question, so
// `payload.answers` could not correct it. `Glossary` is now exact structural
// evidence only: a `glossary` path segment, `glossary.md`, or `CONTEXT.md`. A
// term-definition document outside those falls through to the batched `type`
// question. Nothing replaced the rule: no semantic test, no dominance ratio, no
// percentage threshold.
function inferType(sourcePath, tree, body) {
  const dirs = dirSegments(sourcePath);
  const base = path.posix.basename(sourcePath);
  const stripped = validation.withoutFencedCode(body || '');

  if (dirs.includes('adr') || dirs.includes('decisions') || ADR_FILENAME.test(base) || isAdrTemplate(stripped)) {
    return 'Decision';
  }
  if (dirs.includes('glossary') || base.toLowerCase() === 'glossary.md' || base === 'CONTEXT.md') {
    return 'Glossary';
  }
  if (dirs.includes('constraints') || dirs.includes('constraint')) return 'Constraint';
  if (dirs.includes('research')) return 'Research';
  if (dirs.includes('playbooks') || dirs.includes('playbook') || dirs.includes('runbooks') || dirs.includes('runbook')) return 'Playbook';
  if (dirs.includes('releases') || dirs.includes('release') || RELEASE_FILENAME.test(base)) return 'Release';
  if (dirs.includes('references') || dirs.includes('reference')) return 'Reference';
  if (hasRuntimeField(tree)) return 'Attested Computation';
  return null;
}

// ------------------------------------------------------------------ group placement

function stripExtension(sourcePath) {
  const ext = path.posix.extname(sourcePath);
  return ext ? sourcePath.slice(0, -ext.length) : sourcePath;
}

// #203 (#202): the accepted reader-purpose group decides the destination
// directory, and nothing else does. The type-directory table and the source-path
// fallback both used to place a concept here; both are gone, because #202 settled
// that a concept type, a source directory, a file count, and a directory depth are
// evidence a human may weigh but can never select a group on their own. `group` is
// always an accepted group key (`migration.js` asks for one rather than inferring
// it), so a substantive concept can no longer land at the bundle root at all.
//
// `Glossary` keeps its own filename rule: #202 gives a group at most one
// `glossary.md`, so two glossary-shaped sources accepted into one group both map
// to the same `<group>/glossary`. `migration.js`'s `placed()` only ever checks a
// candidate path against the bundle already published on disk, never against a
// sibling entry in the same plan, so both reach `migrate` undetected -- only
// assembly's `CONCEPT_TARGET_COLLISION` (#147, see `test/issue-147.test.js`)
// catches them, never a silent merge.
function conceptPathFor(sourcePath, type, group) {
  if (type === 'Glossary') return `${group}/glossary`;
  return `${group}/${path.posix.basename(stripExtension(sourcePath))}`;
}

// ------------------------------------------------------------------------- provenance

// Exactly what the source's own frontmatter already declares under `sources`,
// unmodified, or `null`. Never a default, never a repaired shape, never an
// invented entry -- a non-array value is not "structured provenance" per #131 and
// is treated as absent rather than coerced into one.
function extractProvenance(tree) {
  return tree && Array.isArray(tree.sources) ? tree.sources : null;
}

// ------------------------------------------------------------------------ link rewriting

const LINK_PATTERN = /(\[[^\]\n]*\]\()(\s*)(?:<([^>\n]*)>|([^\s)\n]+))/g;

// #188: a link target's *project-relative identity* is always resolved against
// the source file's own directory -- or, for a root-relative `/foo` target,
// against the repository root directly -- never against the bundle. Shared by
// `rewriteLinks` below and `migration.js`'s own unresolved-link check, so the two
// never compute two different answers for what a link "means".
function resolveLinkTarget(sourcePath, targetPath) {
  return targetPath.startsWith('/')
    ? path.posix.normalize(targetPath.slice(1))
    : path.posix.normalize(path.posix.join(path.posix.dirname(sourcePath), targetPath));
}

function normalizedLinkTarget(sourcePath, resource) {
  const targetPath = validation.bodyLinkPath(resource);
  return targetPath ? resolveLinkTarget(sourcePath, targetPath) + resource.slice(targetPath.length) : null;
}

// Rewrites every link this migration can parse unambiguously: a parsed,
// non-fenced, non-inline-code, non-image standard Markdown inline link (reusing
// `validation.js`'s own `withoutFencedCode`/`bodyLinkPath` rather than a second
// parser). Anything else -- an external URL, an anchor, fenced or inline code, an
// image -- is left exactly as written, byte for byte.
//
// #188: once a link's target identity is resolved (`resolveLinkTarget`, above),
// it splits two ways:
//   - the target is itself another source this same call is migrating
//     (`conceptOf` has it): the link becomes a path to that target's concept,
//     computed bundle-relative-to-bundle-relative (unaffected by either file's
//     depth in the project -- this half was never the bug);
//   - the target is not in `conceptOf` (a project file staying put, or a path
//     that resolves to nothing at all): the link is re-expressed as a path from
//     the concept's own new directory -- `bundleDir/ownConceptDir` -- to that
//     same project-relative identity, so a depth change between the source's old
//     directory and the concept's new one no longer breaks it.
//
// This function never touches disk and never decides whether a target actually
// exists -- it is a pure string transform. Whether a rewritten link resolves to
// nothing is `migration.js`'s `deriveUnresolvedLinks`' job (it has the `services`
// and `gitRoot` this function deliberately does not), surfaced as the
// `plan_link_unresolved` finding `setup.js` adds to `migration-plan`'s response --
// before anything is written, per the brief's "report ... before publication".
//
// Reference-style link *definitions* (`[label]: target`) are out of scope: neither
// shared helper parses that syntax, and writing a second link parser to reach it
// is exactly what reusing `validation.js` is meant to avoid. A document using that
// syntax simply keeps those links unrewritten, which is safe (unresolved is a
// tolerated warning, never silent corruption) rather than lossy.
//
// `conceptOf` is a `Map<sourcePath, conceptPath>` covering every source this same
// migration call is placing (`disposition: "migrate"`), keyed by the source's own
// project-relative path -- exactly the identity #22/#131 already use. `bundleDir`
// is the bundle root's own project-relative path (e.g. `"okf"`), so a concept's
// project-relative directory is always `bundleDir/dirname(concept.md)`.
function rewriteLinks(sourcePath, body, conceptOf, bundleDir) {
  const ownConcept = conceptOf.get(sourcePath);
  const lines = body.split('\n');
  const maskLines = validation.withoutFencedCode(body).split('\n');

  return lines.map((line, index) => {
    if (line !== '' && maskLines[index] === '') return line; // inside a fenced code block
    return line.replace(LINK_PATTERN, (whole, prefix, ws, angled, bare, offset) => {
      if (line[offset - 1] === '!' || line[offset - 1] === '\\') return whole; // image or escaped
      const before = line.slice(0, offset);
      if (((before.match(/`/g) || []).length) % 2 === 1) return whole; // inside inline code

      const rawTarget = angled !== undefined ? angled : bare;
      const targetPath = validation.bodyLinkPath(rawTarget);
      if (!targetPath || !ownConcept) return whole;

      const resolved = resolveLinkTarget(sourcePath, targetPath);
      const targetConcept = conceptOf.get(resolved);
      const ownConceptDir = path.posix.dirname(`${ownConcept}.md`);
      const newTargetPath = targetConcept
        ? path.posix.relative(ownConceptDir, `${targetConcept}.md`)
        : path.posix.relative(path.posix.join(bundleDir, ownConceptDir), resolved);

      // The `?query`/`#fragment` suffix identifies a section of the target, not the
      // target itself: it is carried through untouched (#159). `bodyLinkPath`
      // returns only the path part, so whatever follows it in the raw target is
      // exactly that suffix.
      const suffix = rawTarget.slice(targetPath.length);
      const newTarget = newTargetPath + suffix;
      return `${prefix}${ws}${angled !== undefined ? `<${newTarget}>` : newTarget}`;
    });
  }).join('\n');
}

module.exports = {
  inferType, conceptPathFor, extractProvenance,
  rewriteLinks, resolveLinkTarget, normalizedLinkTarget,
};
