import { promises as fs } from "node:fs";
import type { Dirent } from "node:fs";
import { basename, join, relative } from "node:path";

import type {
  TmpaGovernanceObject,
  TmpaReference,
  TmpaRisk,
} from "@codeflowmu/protocol";

import {
  listField,
  parseMarkdownFrontmatter,
  strField,
} from "../ledger/frontmatter.ts";
import {
  canonicalBytes,
  compareUnicodeCodePoints,
  finalizeTmpaObject,
  sha256Digest,
  stableValue,
} from "./canonical.ts";
import {
  createCodeFlowMuTmpaProfile,
  type TmpaRoleAssignment,
} from "./profile.ts";
import type {
  FcopTmpaProjectionResult,
  TmpaProjectedCandidate,
} from "./types.ts";
import {
  readTmpaSourceRevisionRecords,
  type TmpaSourceRevisionRecord,
} from "./SourceRevisionStore.ts";

const PROJECTED_PREFIX_TYPES: Record<string, string> = {
  TASK: "TASK",
  REPORT: "REPORT",
  REVIEW: "REVIEW",
  ISSUE: "ISSUE",
  DECISION: "DECISION",
  APPROVAL: "DECISION",
  CORRECTION: "CORRECTION",
  FAILURE: "FAILURE",
  RECOVERY: "RECOVERY",
  INSPECTION: "INSPECTION",
  ALERT: "ALERT",
  OBSERVATION: "INSPECTION",
};

const SOURCE_DIRS = [
  "fcop/tasks",
  "fcop/reports",
  "fcop/reviews",
  "fcop/issues",
  "fcop/log",
  "fcop/_lifecycle/inbox",
  "fcop/_lifecycle/active",
  "fcop/_lifecycle/review",
  "fcop/_lifecycle/done",
  "fcop/_lifecycle/archive",
] as const;

interface ProjectionDraft {
  source_id: string;
  path: string;
  raw_source_digest: string;
  object: TmpaGovernanceObject;
}

interface TeamMember {
  agent_id?: string;
  role?: string;
}

function normalizeSlashes(value: string): string {
  return value.replace(/\\/g, "/");
}

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

function bodyFromMarkdown(raw: string): string {
  if (!raw.startsWith("---")) return raw;
  const match = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
  return match ? raw.slice(match[0].length) : raw;
}

function plainJson<T>(value: T): T {
  return JSON.parse(JSON.stringify(value)) as T;
}

function typeFromFilename(filename: string): string | null {
  const prefix = basename(filename).split("-", 1)[0]?.toUpperCase() ?? "";
  return PROJECTED_PREFIX_TYPES[prefix] ?? null;
}

function physicalLifecycleState(path: string): string | null {
  const normalized = normalizeSlashes(path).toLowerCase();
  const match = normalized.match(
    /\/fcop\/_lifecycle\/(inbox|active|review|done|archive)\//,
  );
  return match?.[1] ?? null;
}

function fallbackTimestampFromId(id: string): string {
  const match = id.match(/-(\d{4})(\d{2})(\d{2})-/);
  if (!match) return "1970-01-01T00:00:00.000Z";
  return `${match[1]}-${match[2]}-${match[3]}T00:00:00.000Z`;
}

function validTimestamp(value: unknown, fallbackId: string): string {
  if (value instanceof Date && Number.isFinite(value.getTime())) {
    return value.toISOString();
  }
  if (typeof value === "string" && Number.isFinite(Date.parse(value))) {
    return new Date(value).toISOString();
  }
  return fallbackTimestampFromId(fallbackId);
}

function recordId(type: string, filename: string, fm: Record<string, unknown>): string {
  const keysByType: Record<string, string[]> = {
    TASK: ["task_id"],
    REPORT: ["report_id"],
    REVIEW: ["review_id"],
    ISSUE: ["issue_id"],
    DECISION: ["decision_id", "approval_id"],
    CORRECTION: ["correction_id"],
    FAILURE: ["failure_id"],
    RECOVERY: ["recovery_id"],
    INSPECTION: ["inspection_id", "observation_id"],
    ALERT: ["alert_id"],
  };
  for (const key of keysByType[type] ?? []) {
    const value = stripExtension(strField(fm, key));
    if (value) return value;
  }
  return stripExtension(basename(filename));
}

function findTaskReference(fm: Record<string, unknown>): string {
  const directKeys = [
    "source_task_id",
    "task_id",
    "parent_task_id",
    "parent_task",
    "ref_task",
  ];
  for (const key of directKeys) {
    const value = stripExtension(strField(fm, key));
    if (value) return value;
  }
  const subject = stripExtension(
    strField(fm, "subject_ref") || strField(fm, "subject_id"),
  );
  if (/^TASK-/i.test(subject)) return subject;
  return (
    listField(fm, "references")
      .map(stripExtension)
      .find((value) => /^TASK-/i.test(value)) ?? ""
  );
}

function roleForObject(type: string, fm: Record<string, unknown>): string {
  if (type === "REVIEW") {
    return (
      strField(fm, "reviewer_role") ||
      strField(fm, "reviewer") ||
      strField(fm, "sender") ||
      "UNBOUND"
    ).toUpperCase();
  }
  return (
    strField(fm, "sender") ||
    strField(fm, "role") ||
    strField(fm, "created_by") ||
    "UNBOUND"
  ).toUpperCase();
}

function actionsForRole(role: string): string[] {
  switch (role.toUpperCase()) {
    case "PM":
      return [
        "runtime_dispatch",
        "submit_review",
        "approve_review",
        "reject_review",
        "reopen_task",
        "archive_task",
        "force_archive_task",
      ];
    case "QA":
      return ["submit_review", "approve_review", "reject_review"];
    case "DEV":
    case "OPS":
      return ["submit_review"];
    default:
      return [];
  }
}

async function readTeamAssignments(projectRoot: string): Promise<{
  assignments: TmpaRoleAssignment[];
  identityByRole: Map<string, string>;
}> {
  const identityByRole = new Map<string, string>();
  const assignments: TmpaRoleAssignment[] = [];
  try {
    const raw = await fs.readFile(join(projectRoot, "codeflowmu.team.json"), "utf8");
    const parsed = JSON.parse(raw) as { members?: TeamMember[] };
    for (const member of parsed.members ?? []) {
      const identity = String(member.agent_id ?? "").trim();
      const role = String(member.role ?? "").trim().toUpperCase();
      if (!identity || !role) continue;
      if (!identityByRole.has(role)) identityByRole.set(role, identity);
      assignments.push({ identity, role, actions: actionsForRole(role) });
    }
  } catch {
    // Missing application registry is represented later as AUTHORITY_UNDETERMINED.
  }
  return { assignments, identityByRole };
}

function creatorForRole(
  role: string,
  fm: Record<string, unknown>,
  identityByRole: ReadonlyMap<string, string>,
): string {
  const explicitKeys = [
    "reviewer_agent",
    "agent_id",
    "dispatch_agent_id",
    "created_by_agent",
    "session_agent_id",
  ];
  for (const key of explicitKeys) {
    const value = strField(fm, key);
    if (value) return value;
  }
  if (role === "ADMIN") return "human:ADMIN";
  if (role === "SYSTEM" || role === "CODEFLOWMU") return "CODEFLOWMU-RUNTIME";
  return identityByRole.get(role) ?? `unbound:${role}`;
}

function actorIdentity(
  actor: string,
  identityByRole: ReadonlyMap<string, string>,
): { creator: string; role: string } {
  const normalized = actor.trim();
  const upper = normalized.toUpperCase();
  if (!normalized || upper === "CODEFLOWMU" || upper === "SYSTEM") {
    return { creator: "CODEFLOWMU-RUNTIME", role: "SYSTEM" };
  }
  if (upper === "ADMIN") return { creator: "human:ADMIN", role: "ADMIN" };
  if (identityByRole.has(upper)) {
    return { creator: identityByRole.get(upper)!, role: upper };
  }
  for (const [role, identity] of identityByRole) {
    if (identity.toUpperCase() === upper) return { creator: identity, role };
  }
  return { creator: `unbound:${normalized}`, role: upper };
}

function addReference(
  references: TmpaReference[],
  relation: string,
  target: unknown,
): void {
  const normalized = stripExtension(String(target ?? ""));
  if (!relation || !normalized) return;
  if (
    !references.some(
      (reference) =>
        reference.relation === relation && reference.target === normalized,
    )
  ) {
    references.push({ relation, target: normalized });
  }
}

function referencesForObject(
  type: string,
  fm: Record<string, unknown>,
  workId: string,
): TmpaReference[] {
  const references: TmpaReference[] = [];
  if (type === "REPORT") addReference(references, "reports", workId);
  if (type === "REVIEW") {
    addReference(
      references,
      "reviews",
      strField(fm, "subject_ref") || strField(fm, "subject_id") || workId,
    );
  }
  if (type === "ISSUE") {
    addReference(
      references,
      "relates_to",
      strField(fm, "source_report") || workId,
    );
  }
  if (type === "DECISION") {
    const decision = strField(fm, "decision").toLowerCase();
    const relation = ["approve", "approved", "accepted", "pass"].includes(decision)
      ? "accepts"
      : "relates_to";
    addReference(
      references,
      relation,
      strField(fm, "subject_ref") || strField(fm, "subject_id") || workId,
    );
  }

  addReference(
    references,
    "parent",
    strField(fm, "parent") ||
      strField(fm, "parent_task_id") ||
      strField(fm, "parent_task"),
  );
  for (const target of listField(fm, "references")) {
    addReference(references, "references", target);
  }
  for (const target of listField(fm, "depends_on")) {
    addReference(references, "depends_on", target);
  }
  for (const target of listField(fm, "reports")) {
    addReference(references, "reports", target);
  }
  for (const target of listField(fm, "based_on")) {
    addReference(references, "based_on", target);
  }
  addReference(references, "supersedes", strField(fm, "supersedes"));
  addReference(references, "resolves", strField(fm, "resolves"));
  addReference(
    references,
    "human_approves",
    strField(fm, "governance_decision_id") ||
      strField(fm, "fact_check_decision_id"),
  );

  return references.sort((left, right) =>
    compareUnicodeCodePoints(
      `${left.relation}\u0000${left.target}`,
      `${right.relation}\u0000${right.target}`,
    ),
  );
}

function riskFromFrontmatter(fm: Record<string, unknown>): TmpaRisk | undefined {
  const raw = strField(fm, "risk_level").toLowerCase();
  if (!["low", "medium", "high", "irreversible"].includes(raw)) return undefined;
  const level = raw as TmpaRisk["level"];
  return {
    level,
    requires_human_approval:
      level === "high" ||
      level === "irreversible" ||
      fm.requires_human_approval === true,
  };
}

async function walkMarkdownFiles(root: string): Promise<string[]> {
  let entries: Dirent[];
  try {
    entries = await fs.readdir(root, { withFileTypes: true });
  } catch {
    return [];
  }
  const files: string[] = [];
  for (const entry of entries.sort((left, right) =>
    compareUnicodeCodePoints(left.name, right.name),
  )) {
    const full = join(root, entry.name);
    if (entry.isDirectory()) files.push(...(await walkMarkdownFiles(full)));
    else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) files.push(full);
  }
  return files;
}

function transitionReferences(
  taskId: string,
  transition: Record<string, unknown>,
  previousTransitionId: string | undefined,
): TmpaReference[] {
  const references: TmpaReference[] = [];
  if (previousTransitionId) addReference(references, "depends_on", previousTransitionId);
  const action = String(transition.action ?? "");
  const report = transition.report;
  if (action === "submit_review") addReference(references, "reports", report);
  else if (action === "approve_review") addReference(references, "accepts", report);
  else if (action === "archive_task") addReference(references, "archives", taskId);
  for (const target of Array.isArray(transition.based_on)
    ? transition.based_on
    : transition.based_on
      ? [transition.based_on]
      : []) {
    addReference(references, "based_on", target);
  }
  return references.sort((left, right) =>
    compareUnicodeCodePoints(
      `${left.relation}\u0000${left.target}`,
      `${right.relation}\u0000${right.target}`,
    ),
  );
}

function resequenceDrafts(drafts: ProjectionDraft[]): void {
  const byStream = new Map<string, ProjectionDraft[]>();
  for (const draft of drafts) {
    const members = byStream.get(draft.object.stream.id) ?? [];
    members.push(draft);
    byStream.set(draft.object.stream.id, members);
  }
  for (const members of byStream.values()) {
    const unique = new Map<string, ProjectionDraft>();
    for (const draft of members) {
      const key = `${draft.object.created_at}\u0000${draft.object.id}`;
      if (!unique.has(key)) unique.set(key, draft);
    }
    const ordered = [...unique.entries()].sort(([left], [right]) =>
      compareUnicodeCodePoints(left, right),
    );
    const sequenceByKey = new Map(
      ordered.map(([key], index) => [key, index + 1]),
    );
    for (const draft of members) {
      const key = `${draft.object.created_at}\u0000${draft.object.id}`;
      draft.object.stream.sequence = sequenceByKey.get(key) ?? 1;
      draft.object.integrity.digest = "";
      draft.object.integrity.digest = finalizeTmpaObject(draft.object).integrity.digest;
    }
  }
}

export async function projectFcopToTmpa(
  projectRoot: string,
): Promise<FcopTmpaProjectionResult> {
  const { assignments, identityByRole } = await readTeamAssignments(projectRoot);
  const profile = createCodeFlowMuTmpaProfile({ assignments });
  const revisionRecords = await readTmpaSourceRevisionRecords(projectRoot);
  const revisionsByObject = new Map<string, TmpaSourceRevisionRecord[]>();
  for (const record of revisionRecords) {
    const members = revisionsByObject.get(record.object_id) ?? [];
    members.push(record);
    revisionsByObject.set(record.object_id, members);
  }
  const fileSet = new Set<string>();
  for (const sourceDir of SOURCE_DIRS) {
    for (const file of await walkMarkdownFiles(join(projectRoot, ...sourceDir.split("/")))) {
      fileSet.add(file);
    }
  }
  const files = [...fileSet].sort((left, right) =>
    compareUnicodeCodePoints(
      normalizeSlashes(relative(projectRoot, left)),
      normalizeSlashes(relative(projectRoot, right)),
    ),
  );

  const drafts: ProjectionDraft[] = [];
  for (const file of files) {
    const type = typeFromFilename(file);
    if (!type) continue;
    const liveRaw = await fs.readFile(file, "utf8");
    const liveFm = parseMarkdownFrontmatter(liveRaw);
    const id = recordId(type, file, liveFm);
    const objectRevisions = type === "TASK" ? revisionsByObject.get(id) ?? [] : [];
    const primaryRevision = objectRevisions[0];
    const raw = primaryRevision?.raw_text ?? liveRaw;
    const fm = parseMarkdownFrontmatter(raw);
    const workId = type === "TASK" ? id : findTaskReference(fm) || id;
    const parentId = stripExtension(
      strField(fm, "parent") ||
        strField(fm, "parent_task_id") ||
        strField(fm, "parent_task"),
    );
    const role = roleForObject(type, fm);
    const creator = creatorForRole(role, fm, identityByRole);
    const sourcePath = normalizeSlashes(relative(projectRoot, file));
    const rawDigest = sha256Digest(Buffer.from(raw, "utf8"));
    const liveRawDigest = sha256Digest(Buffer.from(liveRaw, "utf8"));
    const createdAt = validTimestamp(
      fm.created_at ?? fm.decided_at ?? fm.updated_at,
      id,
    );
    const physicalState = physicalLifecycleState(file);
    const body = bodyFromMarkdown(raw);
    const contentBody = {
      ...plainJson(stableValue(fm)),
      markdown_body: body,
    };
    const baseObject = finalizeTmpaObject({
      tmpa_version: "S1.0",
      id,
      type,
      governed_work: {
        id: workId,
        primary_carrier_id: workId,
        ...(parentId ? { parent_id: parentId } : {}),
        ...(strField(fm, "thread_key")
          ? { thread_id: strField(fm, "thread_key") }
          : {}),
      },
      stream: { id: `fcop:${creator}`, sequence: 1 },
      creator,
      role,
      created_at: createdAt,
      lifecycle: {
        profile: profile.id,
        state: type === "TASK" ? profile.initial_state : physicalState ?? profile.initial_state,
      },
      references: referencesForObject(type, fm, workId),
      ...(riskFromFrontmatter(fm) ? { risk: riskFromFrontmatter(fm) } : {}),
      content: {
        media_type: "text/markdown",
        body: contentBody,
      },
      extensions: {
        source_profile: "fcop-markdown-v3",
        source_path: sourcePath,
        source_byte_digest: rawDigest,
        stream_sequence_source: "deterministic_projection_order",
        ...(physicalState ? { observed_lifecycle_state: physicalState } : {}),
      },
    });
    drafts.push({
      source_id: primaryRevision
        ? `.codeflowmu/tmpa/source-revisions/${id}/${primaryRevision.content_digest.slice("sha256:".length)}.json#primary`
        : sourcePath,
      path: sourcePath,
      raw_source_digest: rawDigest,
      object: baseObject,
    });

    if (type !== "TASK") continue;
    let previousRevisionObjectId = id;
    for (const revision of objectRevisions.slice(1)) {
      const revisionFm = parseMarkdownFrontmatter(revision.raw_text);
      const revisionRole = roleForObject("TASK", revisionFm);
      const revisionCreator = creatorForRole(
        revisionRole,
        revisionFm,
        identityByRole,
      );
      const revisionId = `${id}::revision:${revision.content_digest.slice(-12)}`;
      const revisionObject = finalizeTmpaObject({
        tmpa_version: "S1.0",
        id: revisionId,
        type: "CORRECTION",
        governed_work: {
          id,
          primary_carrier_id: id,
          ...(parentId ? { parent_id: parentId } : {}),
          ...(strField(revisionFm, "thread_key")
            ? { thread_id: strField(revisionFm, "thread_key") }
            : {}),
        },
        stream: { id: `fcop:${revisionCreator}`, sequence: 1 },
        creator: revisionCreator,
        role: revisionRole,
        created_at: validTimestamp(revision.captured_at, revisionId),
        lifecycle: {
          profile: profile.id,
          state: strField(revisionFm, "state") || profile.initial_state,
        },
        references: [
          { relation: "supersedes", target: previousRevisionObjectId },
        ],
        content: {
          media_type: "text/markdown",
          body: {
            ...plainJson(stableValue(revisionFm)),
            markdown_body: bodyFromMarkdown(revision.raw_text),
          },
        },
        extensions: {
          source_profile: "codeflowmu-content-addressed-task-revision-v1",
          source_path: revision.source_path,
          source_byte_digest: revision.content_digest,
          capture_reason: revision.capture_reason,
          stream_sequence_source: "deterministic_projection_order",
        },
      });
      drafts.push({
        source_id: `.codeflowmu/tmpa/source-revisions/${id}/${revision.content_digest.slice("sha256:".length)}.json#correction`,
        path: revision.source_path,
        raw_source_digest: revision.content_digest,
        object: revisionObject,
      });
      previousRevisionObjectId = revisionId;
    }

    const transitions = Array.isArray(liveFm.transitions)
      ? (liveFm.transitions as Array<Record<string, unknown>>)
      : [];
    let previousTransitionId: string | undefined;
    for (const [index, transition] of transitions.entries()) {
      const transitionTuple = {
        task_id: id,
        index: index + 1,
        from: String(transition.from ?? ""),
        to: String(transition.to ?? ""),
        action: String(transition.action ?? ""),
        at: String(transition.at ?? ""),
      };
      const suffix = sha256Digest(canonicalBytes(transitionTuple)).slice(-12);
      const transitionId = `${id}::transition:${String(index + 1).padStart(4, "0")}:${suffix}`;
      const actor = actorIdentity(String(transition.by ?? "SYSTEM"), identityByRole);
      const transitionObject = finalizeTmpaObject({
        tmpa_version: "S1.0",
        id: transitionId,
        type: "TRANSITION",
        governed_work: {
          id,
          primary_carrier_id: id,
          ...(parentId ? { parent_id: parentId } : {}),
          ...(strField(liveFm, "thread_key")
            ? { thread_id: strField(liveFm, "thread_key") }
            : {}),
        },
        stream: { id: `fcop:${actor.creator}`, sequence: 1 },
        creator: actor.creator,
        role: actor.role,
        created_at: validTimestamp(transition.at, transitionId),
        lifecycle: {
          profile: profile.id,
          state: String(transition.to ?? profile.initial_state),
          transition: {
            from: String(transition.from ?? ""),
            action: String(transition.action ?? ""),
            to: String(transition.to ?? ""),
          },
        },
        references: transitionReferences(id, transition, previousTransitionId),
        content: {
          media_type: "application/json",
          body: plainJson(stableValue(transition)),
        },
        extensions: {
          source_profile: "fcop-embedded-transition-v3",
          source_path: sourcePath,
          source_fragment: `transitions/${index}`,
          source_byte_digest: liveRawDigest,
          stream_sequence_source: "deterministic_projection_order",
        },
      });
      drafts.push({
        source_id: `${sourcePath}#transition-${index + 1}`,
        path: sourcePath,
        raw_source_digest: liveRawDigest,
        object: transitionObject,
      });
      previousTransitionId = transitionId;
    }

    if (physicalState) {
      const observationId = `${id}::state-observation:${liveRawDigest.slice(-12)}`;
      const observationObject = finalizeTmpaObject({
        tmpa_version: "S1.0",
        id: observationId,
        type: "STATE_OBSERVATION",
        governed_work: {
          id,
          primary_carrier_id: id,
          ...(parentId ? { parent_id: parentId } : {}),
          ...(strField(liveFm, "thread_key")
            ? { thread_id: strField(liveFm, "thread_key") }
            : {}),
        },
        stream: { id: "fcop:CODEFLOWMU-RUNTIME", sequence: 1 },
        creator: "CODEFLOWMU-RUNTIME",
        role: "SYSTEM",
        created_at: validTimestamp(
          liveFm.updated_at ?? liveFm.archived_at,
          observationId,
        ),
        lifecycle: { profile: profile.id, state: physicalState },
        references: [],
        content: {
          media_type: "application/json",
          body: { state: physicalState, path: sourcePath },
        },
        extensions: {
          source_profile: "fcop-lifecycle-path-observation-v3",
          source_path: sourcePath,
          source_byte_digest: liveRawDigest,
          stream_sequence_source: "deterministic_projection_order",
        },
      });
      drafts.push({
        source_id: `${sourcePath}#state-observation`,
        path: sourcePath,
        raw_source_digest: liveRawDigest,
        object: observationObject,
      });
    }
  }

  resequenceDrafts(drafts);
  const sources: TmpaProjectedCandidate[] = drafts
    .map((draft) => ({
      source_id: draft.source_id,
      path: draft.path,
      media_type: "application/json",
      bytes: canonicalBytes(draft.object),
      object: draft.object,
      extensions: { raw_source_digest: draft.raw_source_digest },
    }))
    .sort((left, right) => compareUnicodeCodePoints(left.source_id, right.source_id));

  return {
    profile,
    sources,
    source_file_count: files.length,
    projected_object_count: sources.length,
  };
}
