/**
 * parse.ts — Parse entité.md files to extract modules and cross-module dependencies.
 *
 * Reads every entité.md at Module level across the selected applications.
 * For each module, extracts entity definitions and cross-module FK references
 * from the attribute table. Core/SmartStack refs are collected separately
 * (excluded from the dependency graph).
 */
import path from 'node:path';
import { readText, fileExists } from '../../../../lib/fs.js';
import type {
  PlanDevelopmentInput,
  ModuleInfo,
  EntityRef,
  CrossModuleDep,
  CoreRef,
  ReadinessInfo,
  ModuleKey,
} from './types.js';
import { moduleKey } from './types.js';

// ---------------------------------------------------------------------------
// Regex patterns
// ---------------------------------------------------------------------------

/** Match entity headings: ### ENT-001 — Opportunity (agrégat racine) */
const ENTITY_HEADING_RE = /^### (ENT-\d+)\s*—\s*(\w+)(?:\s*\(([^)]+)\))?/gm;

/**
 * Match Guid attributes in the attribute table.
 * Captures: attributeName, full contraintes cell content.
 * Tolerates varying whitespace in markdown table rows.
 */
const GUID_ATTR_RE = /^\|\s*(\w+)\s*\|\s*[Gg]uid\s*\|\s*(.+?)\s*\|/gm;

/**
 * Match cross-module FK description in the Contraintes column.
 * Pattern: FK cross-module vers APP/MODULE.Entity
 * Also matches: FK cross module vers (without hyphen)
 */
const CROSS_MODULE_FK_RE =
  /FK\s+cross[- ]?module\s+vers\s+([A-Z][A-Z0-9_-]*)\/([A-Z][A-Z0-9_-]*)\.(\w+)/i;

/**
 * Match SmartStack Core refs to exclude from the dependency graph.
 * Patterns: FK Core auth_Users, FK cross-module vers auth_Users, auth_*, nav_*, tnt_*
 */
const CORE_REF_RE = /(?:FK\s+(?:Core\s+)?)?(?:cross[- ]?module\s+vers\s+)?(auth_\w+|nav_\w+|tnt_\w+)/i;

/** Detect placeholder entité.md (not yet authored). */
const PLACEHOLDER_RE = /_À définir lors de la phase/;

/**
 * Match depends= attribute in module index.md anchor.
 * Pattern: <!-- ba:node kind=node level=module code=X depends=APP/MOD,APP/MOD -->
 */
const CONTEXT_DEPS_RE = /<!--\s*ba:node\b[^>]*\bdepends=([\w/,]+)/;

/**
 * Parse audit verdict line from _audit/prd.md.
 * Pattern: _<date> · Verdict : GO · score 88/100 · 0 err · 2 warn · 15 ok_
 */
const VERDICT_RE =
  /Verdict\s*:\s*(GO|NO-GO)(?:.*?score\s+(\d+))?(?:.*?(\d+)\s+err)?(?:.*?(\d+)\s+warn)?/;

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/**
 * Parse all modules across the selected applications.
 * Returns one ModuleInfo per discovered module.
 */
export async function parseModules(
  spec: PlanDevelopmentInput,
  discoveredModules: Map<string, string[]>,
): Promise<{ modules: ModuleInfo[]; warnings: string[] }> {
  const modules: ModuleInfo[] = [];
  const warnings: string[] = [];

  for (const [appCode, moduleCodes] of discoveredModules) {
    for (const modCode of moduleCodes) {
      const info = await parseOneModule(spec, appCode, modCode);
      modules.push(info.module);
      warnings.push(...info.warnings);
    }
  }

  return { modules, warnings };
}

// ---------------------------------------------------------------------------
// Internal
// ---------------------------------------------------------------------------

async function parseOneModule(
  spec: PlanDevelopmentInput,
  appCode: string,
  modCode: string,
): Promise<{ module: ModuleInfo; warnings: string[] }> {
  const warnings: string[] = [];
  const key = moduleKey(appCode, modCode);
  const entitePath = path.join(spec.baRoot, appCode, modCode, 'entité.md');

  const base: ModuleInfo = {
    appCode,
    moduleCode: modCode,
    key,
    entities: [],
    crossModuleDeps: [],
    coreRefs: [],
    hasEntiteMd: false,
  };

  // --- Check entité.md existence ---
  const exists = await fileExists(entitePath);
  if (!exists) {
    warnings.push(`${key}: no entité.md found — data model not yet generated`);
    // Fallback: read context-based deps from index.md (written by /ba-create-ba-order)
    base.crossModuleDeps = await parseContextDeps(spec.baRoot, appCode, modCode);
    if (base.crossModuleDeps.length > 0) {
      warnings.push(`${key}: using ${base.crossModuleDeps.length} context-based dependencies from index.md`);
    }
    if (spec.includeReadiness) {
      base.readiness = await parseReadiness(spec.baRoot, appCode, modCode);
    }
    return { module: base, warnings };
  }

  // --- Read and check for placeholder ---
  const content = await readText(entitePath);
  if (PLACEHOLDER_RE.test(content)) {
    warnings.push(`${key}: entité.md is a placeholder — data model not yet generated`);
    // Fallback: read context-based deps from index.md (written by /ba-create-ba-order)
    base.crossModuleDeps = await parseContextDeps(spec.baRoot, appCode, modCode);
    if (base.crossModuleDeps.length > 0) {
      warnings.push(`${key}: using ${base.crossModuleDeps.length} context-based dependencies from index.md`);
    }
    if (spec.includeReadiness) {
      base.readiness = await parseReadiness(spec.baRoot, appCode, modCode);
    }
    return { module: base, warnings };
  }

  // --- Parse entities ---
  base.hasEntiteMd = true;
  const entities = parseEntities(content);
  base.entities = entities;

  // --- Parse cross-module FKs ---
  const entityBlocks = splitEntityBlocks(content);
  for (const block of entityBlocks) {
    const { crossModuleDeps, coreRefs, blockWarnings } = parseEntityBlock(
      block.entityName,
      block.content,
      key,
    );
    base.crossModuleDeps.push(...crossModuleDeps);
    base.coreRefs.push(...coreRefs);
    warnings.push(...blockWarnings);
  }

  // --- Parse readiness (optional) ---
  if (spec.includeReadiness) {
    base.readiness = await parseReadiness(spec.baRoot, appCode, modCode);
  }

  return { module: base, warnings };
}

/** Extract entity code, name, and classification from headings. */
export function parseEntities(content: string): EntityRef[] {
  const entities: EntityRef[] = [];
  let match: RegExpExecArray | null;
  const re = new RegExp(ENTITY_HEADING_RE.source, ENTITY_HEADING_RE.flags);

  while ((match = re.exec(content)) !== null) {
    entities.push({
      code: match[1],
      name: match[2],
      classification: match[3]?.trim(),
    });
  }

  return entities;
}

/**
 * Split content into per-entity blocks for FK analysis.
 * Each block runs from one `### ENT-NNN` heading to the next (or EOF).
 */
function splitEntityBlocks(
  content: string,
): Array<{ entityName: string; content: string }> {
  const headingRe = /^### (ENT-\d+)\s*—\s*(\w+)/gm;
  const positions: Array<{ name: string; start: number }> = [];
  let m: RegExpExecArray | null;

  while ((m = headingRe.exec(content)) !== null) {
    positions.push({ name: m[2], start: m.index });
  }

  return positions.map((pos, i) => ({
    entityName: pos.name,
    content: content.slice(
      pos.start,
      i + 1 < positions.length ? positions[i + 1].start : undefined,
    ),
  }));
}

/**
 * Parse a single entity block for Guid attributes and classify them as
 * cross-module deps or Core refs.
 */
function parseEntityBlock(
  entityName: string,
  blockContent: string,
  moduleKeyStr: ModuleKey,
): {
  crossModuleDeps: CrossModuleDep[];
  coreRefs: CoreRef[];
  blockWarnings: string[];
} {
  const crossModuleDeps: CrossModuleDep[] = [];
  const coreRefs: CoreRef[] = [];
  const blockWarnings: string[] = [];

  const re = new RegExp(GUID_ATTR_RE.source, GUID_ATTR_RE.flags);
  let match: RegExpExecArray | null;

  while ((match = re.exec(blockContent)) !== null) {
    const attrName = match[1];
    const contraintes = match[2];

    // Skip PK
    if (/\bPK\b/.test(contraintes)) continue;

    // Check for Core ref first (exclude from dependency graph)
    const coreMatch = CORE_REF_RE.exec(contraintes);
    if (coreMatch) {
      coreRefs.push({
        sourceEntity: entityName,
        sourceAttribute: attrName,
        coreTarget: coreMatch[1],
      });
      continue;
    }

    // Check for cross-module FK
    const crossMatch = CROSS_MODULE_FK_RE.exec(contraintes);
    if (crossMatch) {
      crossModuleDeps.push({
        sourceEntity: entityName,
        sourceAttribute: attrName,
        targetApp: crossMatch[1],
        targetModule: crossMatch[2],
        targetEntity: crossMatch[3],
      });
      continue;
    }

    // Guid attribute ending in Id but not documented — possible undocumented FK
    if (attrName.endsWith('Id') && attrName !== 'Id') {
      // Only warn if the contraintes don't mention anything FK-related
      if (!/FK|relation|cross|Core|auth_|nav_|tnt_/i.test(contraintes)) {
        blockWarnings.push(
          `${moduleKeyStr}: ${entityName}.${attrName} is a Guid ending in Id but has no FK documentation (DM-013)`,
        );
      }
    }
  }

  return { crossModuleDeps, coreRefs, blockWarnings };
}

/**
 * Parse context-based dependencies from a module's index.md anchor comment.
 * Used as a fallback when entité.md is missing or a placeholder — the
 * `depends=` attribute is written by /ba-create-ba-order (Phase 1.5).
 *
 * Returns CrossModuleDep[] so the dependency graph can include context-based
 * deps alongside FK-based deps.
 */
async function parseContextDeps(
  baRoot: string,
  appCode: string,
  modCode: string,
): Promise<CrossModuleDep[]> {
  const indexPath = path.join(baRoot, appCode, modCode, 'index.md');
  const exists = await fileExists(indexPath);
  if (!exists) return [];

  const content = await readText(indexPath);
  const match = CONTEXT_DEPS_RE.exec(content);
  if (!match) return [];

  const depsStr = match[1]; // "APP/MOD,APP/MOD"
  const deps: CrossModuleDep[] = [];

  for (const depKey of depsStr.split(',')) {
    const parts = depKey.split('/');
    if (parts.length !== 2) continue;

    deps.push({
      sourceEntity: '(context)',
      sourceAttribute: '(context)',
      targetApp: parts[0],
      targetModule: parts[1],
      targetEntity: '(context)',
    });
  }

  return deps;
}

/**
 * Parse the readiness verdict from _audit/prd.md.
 * Returns UNKNOWN if the file doesn't exist.
 */
async function parseReadiness(
  baRoot: string,
  appCode: string,
  modCode: string,
): Promise<ReadinessInfo> {
  const verdictPath = path.join(baRoot, appCode, modCode, '_audit', 'prd.md');
  const exists = await fileExists(verdictPath);

  if (!exists) {
    return { verdict: 'UNKNOWN' };
  }

  const content = await readText(verdictPath);
  const match = VERDICT_RE.exec(content);

  if (!match) {
    return { verdict: 'UNKNOWN' };
  }

  return {
    verdict: match[1] as 'GO' | 'NO-GO',
    score: match[2] ? Number(match[2]) : undefined,
    errCount: match[3] ? Number(match[3]) : undefined,
    warnCount: match[4] ? Number(match[4]) : undefined,
  };
}
