import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";

import type { ActionEvidenceRecord } from "../logs/actionLogTypes.ts";
import { listRuntimeEventLogPaths } from "../logs/runtimeEventLogPaths.ts";
import type { SessionRecord } from "../types/state.ts";

export type ExecutionProvenanceSourceName =
  | "session_store"
  | "dispatch_attempt_store"
  | "runtime_jsonl"
  | "report_frontmatter"
  | "panel_action_log";

export interface ExecutionBindingTuple {
  task_id?: string;
  root_task_id?: string;
  thread_key?: string;
  session_id?: string;
  run_id?: string;
  attempt_id?: string;
  lease_id?: string;
  agent_id?: string;
  role?: string;
}

export interface ExecutionProvenanceSource {
  source: ExecutionProvenanceSourceName;
  found: boolean;
  binding?: ExecutionBindingTuple;
}

export interface ExecutionProvenanceConflict {
  field: keyof ExecutionBindingTuple;
  left_source: ExecutionProvenanceSourceName;
  left_value: string;
  right_source: ExecutionProvenanceSourceName;
  right_value: string;
}

export interface ExecutionProvenanceResult {
  status: "verified" | "observability_gap" | "conflict" | "unavailable";
  binding: ExecutionBindingTuple;
  sources: ExecutionProvenanceSource[];
  conflicts: ExecutionProvenanceConflict[];
  observability_gaps: string[];
  hard_sources_agree: number;
}

export interface ResolveExecutionProvenanceInput extends ExecutionBindingTuple {
  projectRoot: string;
  /** Runtime-owned SessionStore, which may live outside the project root. */
  sessionStoreDir?: string;
  panelRecords?: ActionEvidenceRecord[];
}

const BINDING_FIELDS: Array<keyof ExecutionBindingTuple> = [
  "task_id",
  "root_task_id",
  "thread_key",
  "session_id",
  "run_id",
  "attempt_id",
  "lease_id",
  "agent_id",
  "role",
];

function str(value: unknown): string {
  return value == null ? "" : String(value).trim();
}

function canonicalTaskId(value: unknown): string {
  const raw = str(value).replace(/\.md$/i, "");
  return /^TASK-\d{8}-\d{3,}/i.exec(raw)?.[0]?.toUpperCase() ?? raw.toUpperCase();
}

function normalized(field: keyof ExecutionBindingTuple, value: unknown): string {
  if (field === "task_id" || field === "root_task_id") return canonicalTaskId(value);
  if (field === "role") return str(value).toUpperCase();
  return str(value);
}

function compactBinding(binding: ExecutionBindingTuple): ExecutionBindingTuple {
  const out: ExecutionBindingTuple = {};
  for (const field of BINDING_FIELDS) {
    const value = normalized(field, binding[field]);
    if (value) out[field] = value;
  }
  return out;
}

function sessionStoreSource(input: ResolveExecutionProvenanceInput): ExecutionProvenanceSource {
  const sessionId = str(input.session_id);
  if (!sessionId) return { source: "session_store", found: false };
  let runtimeLockStore = "";
  try {
    const runtimeLock = JSON.parse(
      readFileSync(join(input.projectRoot, ".codeflowmu", "runtime.lock"), "utf-8"),
    ) as Record<string, unknown>;
    const dataRoot = str(runtimeLock["data_root"]);
    if (dataRoot) runtimeLockStore = join(dataRoot, "sessions");
  } catch {
    /* Runtime lock is optional for isolated tests and offline review. */
  }
  const configuredStores = [
    str(input.sessionStoreDir),
    str(process.env["CODEFLOWMU_SESSION_STORE_DIR"]),
    runtimeLockStore,
  ].filter(Boolean);
  const candidates = [...new Set([
    ...configuredStores.map((dir) => join(dir, `${sessionId}.json`)),
    join(input.projectRoot, ".codeflowmu", "state", "sessions", `${sessionId}.json`),
    join(input.projectRoot, ".codeflowmu", "sessions", `${sessionId}.json`),
  ])];
  const path = candidates.find(existsSync);
  if (!path) return { source: "session_store", found: false };
  try {
    const row = JSON.parse(readFileSync(path, "utf-8")) as SessionRecord;
    const latestRun = row.protocol.runs?.at(-1);
    return {
      source: "session_store",
      found: true,
      binding: compactBinding({
        task_id: row.protocol.task_id,
        root_task_id: row.runtime_root_task_id,
        thread_key: row.runtime_thread_key,
        session_id: row.protocol.session_id,
        run_id: row.runtime_active_run_id ?? latestRun?.run_id,
        attempt_id: row.runtime_attempt_id,
        lease_id: row.runtime_lease_id,
        agent_id: row.protocol.agent_id,
        role: row.runtime_role,
      }),
    };
  } catch {
    return { source: "session_store", found: false };
  }
}

function dispatchSource(input: ResolveExecutionProvenanceInput): ExecutionProvenanceSource {
  const path = join(input.projectRoot, ".codeflowmu", "dispatch", "dispatch-state.json");
  try {
    const state = JSON.parse(readFileSync(path, "utf-8")) as {
      attempts?: Array<Record<string, unknown>>;
      leases?: Array<Record<string, unknown>>;
    };
    const attempts = state.attempts ?? [];
    const target = [...attempts].reverse().find((attempt) =>
      (input.attempt_id && str(attempt["attempt_id"]) === str(input.attempt_id)) ||
      (input.session_id && str(attempt["session_id"]) === str(input.session_id)) ||
      (input.task_id && canonicalTaskId(attempt["task_id"]) === canonicalTaskId(input.task_id)),
    );
    if (!target) return { source: "dispatch_attempt_store", found: false };
    const leaseId = str(target["lease_id"]);
    const lease = (state.leases ?? []).find((item) =>
      (leaseId && str(item["lease_id"]) === leaseId) ||
      str(item["attempt_id"]) === str(target["attempt_id"]),
    );
    return {
      source: "dispatch_attempt_store",
      found: true,
      binding: compactBinding({
        task_id: str(target["task_id"]) || undefined,
        root_task_id: str(target["parent_task_id"]) || undefined,
        thread_key: str(target["thread_key"]) || undefined,
        session_id: str(target["session_id"] ?? lease?.["session_id"]) || undefined,
        attempt_id: str(target["attempt_id"]) || undefined,
        lease_id: str(target["lease_id"] ?? lease?.["lease_id"]) || undefined,
        agent_id: str(target["target_agent_id"] ?? lease?.["agent_id"]) || undefined,
        role: str(target["target_role"]) || undefined,
      }),
    };
  } catch {
    return { source: "dispatch_attempt_store", found: false };
  }
}

function runtimeJsonlSource(input: ResolveExecutionProvenanceInput): ExecutionProvenanceSource {
  let matched: Record<string, unknown> | null = null;
  for (const path of listRuntimeEventLogPaths(input.projectRoot)) {
    let raw = "";
    try {
      raw = readFileSync(path, "utf-8");
    } catch {
      continue;
    }
    for (const line of raw.split(/\r?\n/)) {
      if (!line.trim()) continue;
      try {
        const row = JSON.parse(line) as Record<string, unknown>;
        const payload = (row["payload"] ?? {}) as Record<string, unknown>;
        const sessionId = str(row["session_id"] ?? payload["session_id"]);
        const taskId = canonicalTaskId(row["task_id"] ?? payload["task_id"]);
        if (input.session_id && sessionId !== str(input.session_id)) continue;
        if (input.task_id && taskId && taskId !== canonicalTaskId(input.task_id)) continue;
        matched = { ...payload, ...row };
      } catch {
        /* malformed runtime event */
      }
    }
  }
  if (!matched) return { source: "runtime_jsonl", found: false };
  const payload = (matched["payload"] ?? {}) as Record<string, unknown>;
  return {
    source: "runtime_jsonl",
    found: true,
    binding: compactBinding({
      task_id: str(matched["task_id"] ?? payload["task_id"]) || undefined,
      root_task_id: str(matched["root_task_id"] ?? payload["root_task_id"]) || undefined,
      thread_key: str(matched["thread_key"] ?? payload["thread_key"]) || undefined,
      session_id: str(matched["session_id"] ?? payload["session_id"]) || undefined,
      run_id: str(matched["run_id"] ?? payload["run_id"]) || undefined,
      attempt_id: str(matched["attempt_id"] ?? payload["attempt_id"]) || undefined,
      lease_id: str(matched["lease_id"] ?? payload["lease_id"]) || undefined,
      agent_id: str(matched["agent_id"] ?? payload["agent_id"]) || undefined,
      role: str(matched["role"] ?? payload["role"]) || undefined,
    }),
  };
}

function reportSource(input: ResolveExecutionProvenanceInput): ExecutionProvenanceSource {
  const binding = compactBinding(input);
  return {
    source: "report_frontmatter",
    found: Object.keys(binding).length > 0,
    binding,
  };
}

function panelSource(input: ResolveExecutionProvenanceInput): ExecutionProvenanceSource {
  const record = input.panelRecords?.find((row) =>
    (!input.session_id || row.session_id === input.session_id) &&
    (!input.task_id || canonicalTaskId(row.task_id) === canonicalTaskId(input.task_id)),
  );
  if (!record) return { source: "panel_action_log", found: false };
  return {
    source: "panel_action_log",
    found: true,
    binding: compactBinding({
      task_id: record.task_id,
      thread_key: record.thread_key,
      session_id: record.session_id,
      run_id: record.run_id,
      agent_id: record.agent_id,
      role: record.role,
    }),
  };
}

export function resolveExecutionProvenance(
  input: ResolveExecutionProvenanceInput,
): ExecutionProvenanceResult {
  const sources = [
    sessionStoreSource(input),
    dispatchSource(input),
    runtimeJsonlSource(input),
    reportSource(input),
    panelSource(input),
  ];
  const hard = sources.filter((source) =>
    source.found &&
    ["session_store", "dispatch_attempt_store", "runtime_jsonl"].includes(source.source),
  );
  const comparable = sources.filter((source) => source.found && source.binding);
  const conflicts: ExecutionProvenanceConflict[] = [];
  for (let i = 0; i < comparable.length; i++) {
    for (let j = i + 1; j < comparable.length; j++) {
      const left = comparable[i]!;
      const right = comparable[j]!;
      for (const field of BINDING_FIELDS) {
        const leftValue = normalized(field, left.binding?.[field]);
        const rightValue = normalized(field, right.binding?.[field]);
        if (leftValue && rightValue && leftValue !== rightValue) {
          conflicts.push({
            field,
            left_source: left.source,
            left_value: leftValue,
            right_source: right.source,
            right_value: rightValue,
          });
        }
      }
    }
  }
  const observabilityGaps: string[] = [];
  if (!sources.find((source) => source.source === "panel_action_log")?.found) {
    observabilityGaps.push("panel_action_log_absent");
  }
  for (const sourceName of ["session_store", "dispatch_attempt_store", "runtime_jsonl"] as const) {
    if (!sources.find((source) => source.source === sourceName)?.found) {
      observabilityGaps.push(`${sourceName}_absent`);
    }
  }
  const hardSourcesAgree = conflicts.length === 0 ? hard.length : 0;
  // SessionStore, DispatchAttemptStore, and Runtime JSONL form the durable
  // execution proof. Panel action logs are auxiliary, but none of the three
  // durable sources may be silently substituted by REPORT self-assertion.
  const verified = hardSourcesAgree === 3;
  const status = conflicts.length > 0
    ? "conflict"
    : verified && observabilityGaps.length > 0
      ? "observability_gap"
      : verified
        ? "verified"
        : "unavailable";
  const preferred =
    sources.find((source) => source.source === "session_store" && source.found)?.binding ??
    sources.find((source) => source.source === "dispatch_attempt_store" && source.found)?.binding ??
    sources.find((source) => source.source === "runtime_jsonl" && source.found)?.binding ??
    compactBinding(input);
  return {
    status,
    binding: preferred ?? {},
    sources,
    conflicts,
    observability_gaps: observabilityGaps,
    hard_sources_agree: hardSourcesAgree,
  };
}
