import { createHash } from "node:crypto";

import { NEGATIVE_RULE_IDS } from "../approval/OperationFacts.ts";
import type {
  CanonicalTaskRuntimeConflict,
  CanonicalTaskRuntimeSnapshot,
  CanonicalTaskRuntimeState,
} from "../pm/resolveCanonicalTaskRuntimeState.ts";

export const THREAD_SCHEDULE_SCHEMA_VERSION =
  "codeflowmu.thread_schedule/v1" as const;

export type ThreadScheduleExpectedEventKind =
  | "formal_report_or_session_end"
  | "capacity_available"
  | "session_registration"
  | "formal_report"
  | "technical_recovery"
  | "agent_attention"
  | "approval_decision"
  | "runtime_observation";

export interface ThreadScheduleTaskFact extends Record<string, unknown> {
  task_id?: string;
  filename?: string;
  root_task_id?: string;
  parent?: string;
  parent_task_id?: string;
  thread_key?: string;
  sender?: string;
  recipient?: string;
  subject?: string;
  title?: string;
  scope?: string;
  bucket?: string;
  lifecycle?: string;
  round_id?: string;
  rework_of?: string;
  supersedes?: string | string[];
  depends_on?: string | string[];
  due_at?: string;
  expected_at?: string;
  created_at?: string;
  updated_at?: string;
}

export interface ThreadScheduleEvidenceFact extends Record<string, unknown> {
  task_id?: string;
  root_task_id?: string;
  thread_key?: string;
  report_id?: string;
  issue_id?: string;
  approval_id?: string;
  filename?: string;
  references?: string | string[];
  status?: string;
  created_at?: string;
  updated_at?: string;
}

export interface ThreadScheduleExpectedEvent {
  event_id: string;
  kind: ThreadScheduleExpectedEventKind;
  task_id: string;
  owner_role: string | null;
  due_at: string | null;
  overdue: boolean;
  reason_code: string;
  evidence_refs: string[];
}

export interface ThreadScheduleConflict {
  conflict_id: string;
  task_id: string;
  code: string;
  detail: string;
  sources: string[];
}

export interface ThreadScheduleAttention {
  attention_id: string;
  kind:
    | "runtime_projection_conflict"
    | "authorization_required"
    | "evidence_arrived"
    | "issue_observed"
    | "expected_event_overdue";
  task_id: string | null;
  owner_role: string;
  evidence_refs: string[];
  summary: string;
}

export interface ThreadScheduleWorkItem {
  task_id: string;
  root_task_id: string;
  parent_task_id: string | null;
  thread_key: string;
  sender: string | null;
  recipient: string | null;
  subject: string;
  lifecycle_bucket: string | null;
  round_id: string;
  rework_of: string | null;
  supersedes: string[];
  depends_on: string[];
  due_at: string | null;
  runtime_state: CanonicalTaskRuntimeState | "unknown";
  runtime_reason_code: string;
  report_ids: string[];
  issue_ids: string[];
  approval_ids: string[];
  expected_event_id: string;
}

export interface ThreadScheduleSnapshot {
  schema_version: typeof THREAD_SCHEDULE_SCHEMA_VERSION;
  project_id: string;
  thread_key: string;
  root_task_id: string;
  revision: string;
  source_digest: string;
  generated_at: string;
  work_items: ThreadScheduleWorkItem[];
  expected_events: ThreadScheduleExpectedEvent[];
  conflicts: ThreadScheduleConflict[];
  attention: ThreadScheduleAttention[];
  negative_list: {
    mode: "agent_negative_list";
    rule_count: 15;
    rule_ids: string[];
    final_authority: "ADMIN";
  };
  /** The rail machine exposes facts only. PM/ADMIN owns business judgment. */
  business_decision: null;
}

export interface BuildThreadScheduleSnapshotInput {
  projectId: string;
  threadKey: string;
  rootTaskId: string;
  tasks: ThreadScheduleTaskFact[];
  reports?: ThreadScheduleEvidenceFact[];
  issues?: ThreadScheduleEvidenceFact[];
  approvals?: ThreadScheduleEvidenceFact[];
  runtimeSnapshots?:
    | CanonicalTaskRuntimeSnapshot[]
    | Record<string, CanonicalTaskRuntimeSnapshot>;
  now?: Date;
}

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

function normalizeRef(value: unknown): string {
  return String(value ?? "").replace(/\.md$/i, "").trim();
}

function normalizeRole(value: unknown): string | null {
  const role = String(value ?? "").trim().toUpperCase();
  return role || null;
}

function stringList(value: unknown): string[] {
  const values = Array.isArray(value)
    ? value
    : typeof value === "string"
      ? value.split(/[;,\n]/)
      : [];
  return [...new Set(values.map(normalizeRef).filter(Boolean))].sort((a, b) =>
    a.localeCompare(b, "en"),
  );
}

function taskIdOf(task: ThreadScheduleTaskFact): string {
  return normalizeTaskId(task.task_id ?? task.filename);
}

function rootTaskIdOf(task: ThreadScheduleTaskFact): string {
  return normalizeTaskId(task.root_task_id) || taskIdOf(task);
}

function parentTaskIdOf(task: ThreadScheduleTaskFact): string | null {
  return normalizeTaskId(task.parent_task_id ?? task.parent) || null;
}

function lifecycleBucketOf(task: ThreadScheduleTaskFact): string | null {
  const value = String(task.scope ?? task.bucket ?? task.lifecycle ?? "")
    .trim()
    .toLowerCase();
  return value || null;
}

function evidenceId(row: ThreadScheduleEvidenceFact, kind: "report" | "issue" | "approval"): string {
  const value =
    kind === "report"
      ? row.report_id ?? row.filename
      : kind === "issue"
        ? row.issue_id ?? row.filename
        : row.approval_id ?? row.filename;
  return normalizeRef(value);
}

function evidenceTaskIds(row: ThreadScheduleEvidenceFact): string[] {
  const direct = [row.task_id, row.root_task_id].map(normalizeTaskId).filter(Boolean);
  const references = stringList(row.references)
    .map(normalizeTaskId)
    .filter((value) => value.startsWith("TASK-"));
  return [...new Set([...direct, ...references])];
}

function evidenceBelongsToTask(
  row: ThreadScheduleEvidenceFact,
  taskId: string,
  rootTaskId: string,
  threadKey: string,
): boolean {
  const linked = evidenceTaskIds(row);
  if (linked.length > 0) {
    return linked.includes(taskId) || (taskId === rootTaskId && linked.includes(rootTaskId));
  }
  const rowThread = String(row.thread_key ?? "").trim();
  return Boolean(rowThread && rowThread === threadKey && taskId === rootTaskId);
}

function stableValue(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(stableValue);
  if (value && typeof value === "object") {
    const record = value as Record<string, unknown>;
    return Object.fromEntries(
      Object.keys(record)
        .sort((a, b) => a.localeCompare(b, "en"))
        .map((key) => [key, stableValue(record[key])]),
    );
  }
  return value;
}

function stableSort<T>(values: T[]): T[] {
  return [...values].sort((a, b) =>
    JSON.stringify(stableValue(a)).localeCompare(
      JSON.stringify(stableValue(b)),
      "en",
    ),
  );
}

function digest(value: unknown): string {
  return `sha256:${createHash("sha256")
    .update(JSON.stringify(stableValue(value)))
    .digest("hex")}`;
}

function runtimeMap(
  input: BuildThreadScheduleSnapshotInput["runtimeSnapshots"],
): Map<string, CanonicalTaskRuntimeSnapshot> {
  const snapshots = Array.isArray(input) ? input : Object.values(input ?? {});
  return new Map(
    snapshots.map((snapshot) => [normalizeTaskId(snapshot.task_id), snapshot]),
  );
}

function expectedKind(
  state: CanonicalTaskRuntimeState | "unknown",
): ThreadScheduleExpectedEventKind {
  switch (state) {
    case "running":
      return "formal_report_or_session_end";
    case "waiting_capacity":
      return "capacity_available";
    case "settling":
      return "session_registration";
    case "waiting_report":
      return "formal_report";
    case "recoverable":
      return "technical_recovery";
    case "failed":
    case "blocked":
    case "done":
    case "projection_conflict":
      return "agent_attention";
    case "unknown":
      return "runtime_observation";
  }
}

function isPendingApproval(row: ThreadScheduleEvidenceFact): boolean {
  const status = String(row.status ?? "").trim().toLowerCase();
  return [
    "pending",
    "pending_approval",
    "pending_information",
    "pending_executor",
    "approval_creation_pending",
  ].includes(status);
}

function isOpenIssue(row: ThreadScheduleEvidenceFact): boolean {
  const status = String(row.status ?? "active").trim().toLowerCase();
  return !["closed", "resolved", "fixed", "no_action", "duplicate"].includes(status);
}

function isoOrNull(value: unknown): string | null {
  const text = String(value ?? "").trim();
  return Number.isFinite(Date.parse(text)) ? new Date(text).toISOString() : null;
}

function conflictRows(
  taskId: string,
  conflicts: CanonicalTaskRuntimeConflict[],
): ThreadScheduleConflict[] {
  return conflicts.map((conflict, index) => ({
    conflict_id: `${taskId}:${conflict.code}:${index + 1}`,
    task_id: taskId,
    code: conflict.code,
    detail: conflict.detail,
    sources: [...conflict.sources].sort((a, b) => a.localeCompare(b, "en")),
  }));
}

/**
 * Build a deterministic, read-only thread timetable.
 *
 * This reducer deliberately does not decide acceptance, rework, PASS/FAIL or
 * risk acceptance. It tells Agents what is present, what is expected next and
 * which facts need attention; Agents and ADMIN remain the decision makers.
 */
export function buildThreadScheduleSnapshot(
  input: BuildThreadScheduleSnapshotInput,
): ThreadScheduleSnapshot {
  const threadKey = String(input.threadKey ?? "").trim();
  const rootTaskId = normalizeTaskId(input.rootTaskId);
  if (!threadKey) throw new Error("ThreadScheduleKernel requires threadKey");
  if (!rootTaskId) throw new Error("ThreadScheduleKernel requires rootTaskId");

  const now = input.now ?? new Date();
  const allReports = input.reports ?? [];
  const allIssues = input.issues ?? [];
  const allApprovals = input.approvals ?? [];
  const runtimes = runtimeMap(input.runtimeSnapshots);
  const tasks = input.tasks
    .filter((task) => {
      const taskThread = String(task.thread_key ?? "").trim();
      const taskId = taskIdOf(task);
      return (
        taskThread === threadKey ||
        taskId === rootTaskId ||
        rootTaskIdOf(task) === rootTaskId
      );
    })
    .filter((task) => Boolean(taskIdOf(task)));
  const threadTaskIds = new Set(tasks.map(taskIdOf));
  const belongsToThread = (row: ThreadScheduleEvidenceFact): boolean => {
    const linkedTaskIds = evidenceTaskIds(row);
    if (linkedTaskIds.length > 0) {
      return linkedTaskIds.some(
        (taskId) => taskId === rootTaskId || threadTaskIds.has(taskId),
      );
    }
    return String(row.thread_key ?? "").trim() === threadKey;
  };
  const reports = allReports.filter(belongsToThread);
  const issues = allIssues.filter(belongsToThread);
  const approvals = allApprovals.filter(belongsToThread);

  const expectedEvents: ThreadScheduleExpectedEvent[] = [];
  const conflicts: ThreadScheduleConflict[] = [];
  const attention: ThreadScheduleAttention[] = [];

  const workItems = tasks.map((task): ThreadScheduleWorkItem => {
    const taskId = taskIdOf(task);
    const taskRoot = rootTaskIdOf(task) || rootTaskId;
    const runtime = runtimes.get(taskId);
    const taskReports = reports.filter((row) =>
      evidenceBelongsToTask(row, taskId, taskRoot, threadKey),
    );
    const taskIssues = issues.filter((row) =>
      evidenceBelongsToTask(row, taskId, taskRoot, threadKey),
    );
    const taskApprovals = approvals.filter((row) =>
      evidenceBelongsToTask(row, taskId, taskRoot, threadKey),
    );
    const reportIds = taskReports
      .map((row) => evidenceId(row, "report"))
      .filter(Boolean)
      .sort((a, b) => a.localeCompare(b, "en"));
    const issueIds = taskIssues
      .map((row) => evidenceId(row, "issue"))
      .filter(Boolean)
      .sort((a, b) => a.localeCompare(b, "en"));
    const approvalIds = taskApprovals
      .map((row) => evidenceId(row, "approval"))
      .filter(Boolean)
      .sort((a, b) => a.localeCompare(b, "en"));
    const runtimeState = runtime?.state ?? "unknown";
    const dueAt = isoOrNull(task.due_at ?? task.expected_at);
    const expectedEventId = `expected:${taskId}:${expectedKind(runtimeState)}`;
    const event: ThreadScheduleExpectedEvent = {
      event_id: expectedEventId,
      kind: expectedKind(runtimeState),
      task_id: taskId,
      owner_role: normalizeRole(task.recipient),
      due_at: dueAt,
      overdue: Boolean(dueAt && Date.parse(dueAt) < now.getTime()),
      reason_code: runtime?.reason_code ?? "runtime_snapshot_unavailable",
      evidence_refs: [
        ...reportIds,
        ...issueIds,
        ...approvalIds,
        ...(runtime?.session_id ? [runtime.session_id] : []),
        ...(runtime?.attempt_id ? [runtime.attempt_id] : []),
        ...(runtime?.lease_id ? [runtime.lease_id] : []),
      ].sort((a, b) => a.localeCompare(b, "en")),
    };
    expectedEvents.push(event);

    const taskConflicts = conflictRows(taskId, runtime?.conflicts ?? []);
    conflicts.push(...taskConflicts);
    if (taskConflicts.length > 0 || runtimeState === "projection_conflict") {
      attention.push({
        attention_id: `attention:projection:${taskId}`,
        kind: "runtime_projection_conflict",
        task_id: taskId,
        owner_role: normalizeRole(task.sender) ?? "PM",
        evidence_refs: taskConflicts.map((row) => row.conflict_id),
        summary: "Runtime projections conflict; automatic technical action must wait for Agent inspection.",
      });
    }
    const pendingApprovals = taskApprovals.filter(isPendingApproval);
    if (pendingApprovals.length > 0) {
      const refs = pendingApprovals
        .map((row) => evidenceId(row, "approval"))
        .filter(Boolean)
        .sort((a, b) => a.localeCompare(b, "en"));
      attention.push({
        attention_id: `attention:approval:${taskId}`,
        kind: "authorization_required",
        task_id: taskId,
        owner_role: "ADMIN",
        evidence_refs: refs,
        summary: "A negative-list operation is waiting for ADMIN's final decision.",
      });
      expectedEvents.push({
        event_id: `expected:${taskId}:approval_decision`,
        kind: "approval_decision",
        task_id: taskId,
        owner_role: "ADMIN",
        due_at: null,
        overdue: false,
        reason_code: "negative_list_match_waiting_admin",
        evidence_refs: refs,
      });
    }
    if (reportIds.length > 0) {
      attention.push({
        attention_id: `attention:evidence:${taskId}`,
        kind: "evidence_arrived",
        task_id: taskId,
        owner_role: normalizeRole(task.sender) ?? "PM",
        evidence_refs: reportIds,
        summary: "Formal report evidence is present; the responsible Agent owns interpretation and acceptance.",
      });
    }
    const openIssues = taskIssues.filter(isOpenIssue);
    if (openIssues.length > 0) {
      attention.push({
        attention_id: `attention:issue:${taskId}`,
        kind: "issue_observed",
        task_id: taskId,
        owner_role: normalizeRole(task.sender) ?? "PM",
        evidence_refs: openIssues
          .map((row) => evidenceId(row, "issue"))
          .filter(Boolean)
          .sort((a, b) => a.localeCompare(b, "en")),
        summary: "Open issue evidence is linked to this work item.",
      });
    }
    if (event.overdue) {
      attention.push({
        attention_id: `attention:overdue:${taskId}`,
        kind: "expected_event_overdue",
        task_id: taskId,
        owner_role: normalizeRole(task.sender) ?? "PM",
        evidence_refs: [event.event_id],
        summary: "The expected event is overdue; the Agent decides whether to wait, communicate or revise the plan.",
      });
    }

    return {
      task_id: taskId,
      root_task_id: taskRoot,
      parent_task_id: parentTaskIdOf(task),
      thread_key: String(task.thread_key ?? threadKey).trim() || threadKey,
      sender: normalizeRole(task.sender),
      recipient: normalizeRole(task.recipient),
      subject: String(task.subject ?? task.title ?? task.filename ?? taskId).trim(),
      lifecycle_bucket: lifecycleBucketOf(task),
      round_id: String(task.round_id ?? "initial").trim() || "initial",
      rework_of: normalizeTaskId(task.rework_of) || null,
      supersedes: stringList(task.supersedes).map(normalizeTaskId).filter(Boolean),
      depends_on: stringList(task.depends_on).map(normalizeTaskId).filter(Boolean),
      due_at: dueAt,
      runtime_state: runtimeState,
      runtime_reason_code: runtime?.reason_code ?? "runtime_snapshot_unavailable",
      report_ids: reportIds,
      issue_ids: issueIds,
      approval_ids: approvalIds,
      expected_event_id: expectedEventId,
    };
  });

  const normalizedSources = {
    project_id: input.projectId,
    thread_key: threadKey,
    root_task_id: rootTaskId,
    tasks: stableSort(workItems),
    reports: stableSort(reports),
    issues: stableSort(issues),
    approvals: stableSort(approvals),
    runtime_snapshots: stableSort([...runtimes.values()]),
    negative_rule_ids: [...NEGATIVE_RULE_IDS],
  };
  const sourceDigest = digest(normalizedSources);

  return {
    schema_version: THREAD_SCHEDULE_SCHEMA_VERSION,
    project_id: String(input.projectId ?? "").trim() || "active-project",
    thread_key: threadKey,
    root_task_id: rootTaskId,
    revision: `schedule-${sourceDigest.slice("sha256:".length, "sha256:".length + 16)}`,
    source_digest: sourceDigest,
    generated_at: now.toISOString(),
    work_items: stableSort(workItems),
    expected_events: stableSort(expectedEvents),
    conflicts: stableSort(conflicts),
    attention: stableSort(attention),
    negative_list: {
      mode: "agent_negative_list",
      rule_count: NEGATIVE_RULE_IDS.length as 15,
      rule_ids: [...NEGATIVE_RULE_IDS],
      final_authority: "ADMIN",
    },
    business_decision: null,
  };
}
