/**
 * ParentAnomalyGuard — WP-14.
 * Reject / classify abnormal parent links for formal TASK trees.
 * Display anomalies stay in ProjectGraph; this module is the reject API.
 */

export type ParentRejectCode =
  | "ok"
  | "parent_missing"
  | "parent_self_reference"
  | "parent_cycle"
  | "closed_parent"
  | "invalid_parent_id";

export interface ParentLinkInput {
  taskId: string;
  parent?: string | null;
  /** Known task ids in the project graph. */
  knownTaskIds: ReadonlySet<string> | string[];
  /** Task ids considered closed / archived for child rejection. */
  closedTaskIds?: ReadonlySet<string> | string[];
}

export interface ParentLinkVerdict {
  code: ParentRejectCode;
  accepted: boolean;
  message: string;
}

function asSet(v?: ReadonlySet<string> | string[]): Set<string> {
  if (!v) return new Set();
  if (Array.isArray(v)) {
    return new Set(v.map((s: string) => String(s).toUpperCase()));
  }
  return new Set([...v].map((s: string) => String(s).toUpperCase()));
}

function normId(id: string): string {
  return String(id ?? "")
    .replace(/\.md$/i, "")
    .trim()
    .toUpperCase();
}

/**
 * Validate a single parent edge. Does not mutate ledger files.
 */
export function evaluateParentLink(input: ParentLinkInput): ParentLinkVerdict {
  const taskId = normId(input.taskId);
  const parentRaw = input.parent;
  if (parentRaw == null || String(parentRaw).trim() === "") {
    return { code: "ok", accepted: true, message: "root task (no parent)" };
  }
  const parent = normId(String(parentRaw));
  if (!/^TASK-\d{8}-\d+/i.test(parent)) {
    return {
      code: "invalid_parent_id",
      accepted: false,
      message: `invalid parent id: ${parentRaw}`,
    };
  }
  if (parent === taskId) {
    return {
      code: "parent_self_reference",
      accepted: false,
      message: `task ${taskId} parents itself`,
    };
  }
  const known = asSet(input.knownTaskIds);
  if (!known.has(parent)) {
    return {
      code: "parent_missing",
      accepted: false,
      message: `parent ${parent} not found for ${taskId}`,
    };
  }
  const closed = asSet(input.closedTaskIds);
  if (closed.has(parent)) {
    return {
      code: "closed_parent",
      accepted: false,
      message: `parent ${parent} is closed; refusing child ${taskId}`,
    };
  }
  return { code: "ok", accepted: true, message: "parent link accepted" };
}

/**
 * Detect cycles given an adjacency map taskId → parentId.
 */
export function detectParentCycle(
  edges: Record<string, string | undefined>,
): { hasCycle: boolean; cycleNode?: string } {
  const normEdges = new Map<string, string>();
  for (const [k, v] of Object.entries(edges)) {
    if (v) normEdges.set(normId(k), normId(v));
  }
  for (const start of normEdges.keys()) {
    const seen = new Set<string>();
    let cur: string | undefined = start;
    while (cur) {
      if (seen.has(cur)) {
        return { hasCycle: true, cycleNode: cur };
      }
      seen.add(cur);
      cur = normEdges.get(cur);
    }
  }
  return { hasCycle: false };
}

export function rejectParentOrThrow(input: ParentLinkInput): void {
  const v = evaluateParentLink(input);
  if (!v.accepted) {
    const err = new Error(`[parent_rejected:${v.code}] ${v.message}`);
    err.name = "ParentAnomalyError";
    throw err;
  }
}
