import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { loadRecipeMetadata, loadCleanConfig, loadNormalizeConfig } from '../metadata.js';
import { loadSelectionsConfig } from '../selector.js';
import { resolveRecipeDir } from '../../utils/paths.js';
import { verifyOutput } from './verifier.js';
import { ensureSourceDocx } from './downloader.js';
import {
  loadComputedProfile,
  evaluateComputedProfile,
  buildComputedArtifact,
  buildPassthroughArtifact,
  writeComputedArtifact,
} from './computed.js';
import { normalizeBracketArtifacts } from './bracket-normalizer.js';
import { runFillPipeline } from '../unified-pipeline.js';
import type { RecipeRunOptions, RecipeRunResult } from './types.js';
import type { ComputedValueMap } from './computed.js';

function toComputedValueMap(values: Record<string, unknown>): ComputedValueMap {
  const computedValues: ComputedValueMap = {};
  for (const [key, value] of Object.entries(values)) {
    if (typeof value === 'string' || typeof value === 'boolean') {
      computedValues[key] = value;
      continue;
    }
    if (Array.isArray(value) && value.every((entry) => typeof entry === 'string')) {
      computedValues[key] = [...value];
    }
  }
  return computedValues;
}

/**
 * Run the full recipe pipeline: clean → patch → fill → verify.
 * If no inputPath is provided, auto-downloads the source document from source_url.
 */
export async function runRecipe(options: RecipeRunOptions): Promise<RecipeRunResult> {
  const { recipeId, outputPath, values, keepIntermediate, computedOutPath } = options;
  const recipeDir = resolveRecipeDir(recipeId);

  const metadata = loadRecipeMetadata(recipeDir);
  const cleanConfig = loadCleanConfig(recipeDir);
  const normalizeConfig = loadNormalizeConfig(recipeDir);

  // Load selectionsConfig if selections.json exists (mirrors engine.ts template path)
  const selectionsPath = join(recipeDir, 'selections.json');
  const selectionsConfig = existsSync(selectionsPath)
    ? loadSelectionsConfig(selectionsPath)
    : undefined;

  // Resolve input: explicit path or auto-download
  const inputPath = options.inputPath ?? await ensureSourceDocx(recipeId, metadata);

  // Load replacements.json
  const replacementsPath = join(recipeDir, 'replacements.json');
  const replacements: Record<string, string | { value: string; format?: Record<string, unknown> }> = JSON.parse(readFileSync(replacementsPath, 'utf-8'));
  const replacementFieldNames = new Set<string>();
  for (const rawReplacement of Object.values(replacements)) {
    const replacement = typeof rawReplacement === 'string' ? rawReplacement : rawReplacement.value;
    for (const match of replacement.matchAll(/\{([a-zA-Z0-9_]+)\}/g)) {
      replacementFieldNames.add(match[1]);
    }
  }

  const inputValues = { ...values };
  const computedInputValues = toComputedValueMap(inputValues);
  const computedProfile = loadComputedProfile(recipeDir);
  const computedEvaluation = computedProfile
    ? evaluateComputedProfile(computedProfile, computedInputValues)
    : null;
  const effectiveValues = computedEvaluation
    ? { ...inputValues, ...computedEvaluation.fillValues }
    : inputValues;
  const verificationValues: Record<string, string> = {};
  for (const field of metadata.fields) {
    if (!replacementFieldNames.has(field.name)) {
      continue;
    }
    const value = effectiveValues[field.name];
    if (typeof value === 'string') {
      verificationValues[field.name] = value;
    }
  }
  const computedArtifact = computedEvaluation && computedProfile
    ? buildComputedArtifact({
      recipeId,
      inputValues: computedInputValues,
      evaluated: computedEvaluation,
      profileVersion: computedProfile.version,
    })
    : (computedOutPath ? buildPassthroughArtifact({ recipeId, inputValues: computedInputValues }) : undefined);

  if (computedOutPath && computedArtifact) {
    writeComputedArtifact(computedOutPath, computedArtifact);
  }

  const shouldNormalizeBracketArtifacts =
    options.normalizeBracketArtifacts ?? normalizeConfig.paragraph_rules.length > 0;

  const result = await runFillPipeline({
    inputPath,
    outputPath,
    values: effectiveValues,
    fields: metadata.fields,
    priorityFieldNames: metadata.priority_fields,
    cleanPatch: { cleanConfig, replacements },
    selectionsConfig,
    postProcess: shouldNormalizeBracketArtifacts
      ? async (outputDocPath: string) => {
        await normalizeBracketArtifacts(outputDocPath, outputDocPath, {
          rules: normalizeConfig.paragraph_rules,
          fieldValues: effectiveValues,
        });
      }
      : undefined,
    verify: (p) => verifyOutput(p, verificationValues, replacements, cleanConfig),
    keepIntermediate,
  });

  return {
    outputPath: result.outputPath,
    metadata,
    fieldsUsed: result.fieldsUsed,
    stages: result.stages!,
    computedArtifact,
    computedOutPath,
  };
}

export { cleanDocument } from './cleaner.js';
export type { CleanResult, CleanOptions } from './cleaner.js';
export { patchDocument } from './patcher.js';
export type { PatchResult } from './patcher.js';
export { verifyOutput, normalizeText, extractAllText, countFormattingAnomalies } from './verifier.js';
export { ensureSourceDocx } from './downloader.js';
export { checkRecipeSourceDrift, computeSourceStructureSignature } from './source-drift.js';
export { enumerateTextParts, getGeneralTextPartNames } from './ooxml-parts.js';
export type { OoxmlTextParts } from './ooxml-parts.js';
export type { RecipeRunOptions, RecipeRunResult, VerifyResult, VerifyCheck } from './types.js';
export type { ComputedArtifact, ComputedProfile } from './computed.js';
