import type { DecisionScope } from "../../../schema/src/decisions/scope.js";
import type { ReportData } from "../build/types.js";
import { withReviewDraft } from "./draft.js";
import {
  exactScopeFromFinding,
  policyScopeRequiresReason,
  validateReviewedPolicyScope,
} from "./scope.js";
import type { ReviewDraft, StagedDecisionEntry, UndoStateEntry } from "./types.js";

export type ReviewVerdict = "confirmed_defect" | "not_defect";

export class ReviewDecisionError extends Error {
  readonly code: string;

  constructor(code: string, message: string) {
    super(message);
    this.name = "ReviewDecisionError";
    this.code = code;
  }
}

function findingById(reportData: ReportData, findingId: string) {
  const finding = reportData.findings.find((entry) => entry.id === findingId);
  if (finding === undefined) {
    throw new ReviewDecisionError("unknown-target", `finding not found: ${findingId}`);
  }
  return finding;
}

function stagedEntryFor(
  draft: ReviewDraft,
  findingId: string,
): StagedDecisionEntry | undefined {
  return draft.decisions.find((entry) => entry.findingId === findingId);
}

function pushUndo(draft: ReviewDraft, findingId: string, prior: UndoStateEntry): ReviewDraft {
  const stack = [...(draft.undoStacks[findingId] ?? [])];
  stack.push(prior);
  return {
    ...draft,
    undoStacks: {
      ...draft.undoStacks,
      [findingId]: stack,
    },
  };
}

function withoutUndoStack(draft: ReviewDraft, findingId: string): Record<string, UndoStateEntry[]> {
  return Object.fromEntries(
    Object.entries(draft.undoStacks).filter(([key]) => key !== findingId),
  );
}

function popUndo(draft: ReviewDraft, findingId: string): {
  draft: ReviewDraft;
  prior: UndoStateEntry | undefined;
} {
  const stack = [...(draft.undoStacks[findingId] ?? [])];
  const prior = stack.pop();
  const nextStacks =
    stack.length === 0
      ? withoutUndoStack(draft, findingId)
      : {
          ...withoutUndoStack(draft, findingId),
          [findingId]: stack,
        };
  return {
    draft: {
      ...draft,
      undoStacks: nextStacks,
    },
    prior,
  };
}

function removeDecision(draft: ReviewDraft, findingId: string): ReviewDraft {
  return {
    ...draft,
    decisions: draft.decisions.filter((entry) => entry.findingId !== findingId),
  };
}

function upsertDecision(draft: ReviewDraft, entry: StagedDecisionEntry): ReviewDraft {
  const without = draft.decisions.filter((item) => item.findingId !== entry.findingId);
  return {
    ...draft,
    decisions: [...without, entry].sort((left, right) =>
      left.findingId.localeCompare(right.findingId),
    ),
  };
}

function validateReasonForScope(
  scope: DecisionScope,
  verdict: ReviewVerdict,
  reason: string | undefined,
): void {
  if (verdict === "not_defect" && policyScopeRequiresReason(scope)) {
    if (reason === undefined || reason.trim().length === 0) {
      throw new ReviewDecisionError(
        "reason-required",
        "reviewed-policy scope requires a reason for not_defect decisions",
      );
    }
  }
}

export async function stageDecision(
  reportDir: string,
  reportData: ReportData,
  input: {
    findingId: string;
    verdict: ReviewVerdict;
    reason?: string;
    scope?: DecisionScope;
    decidedAt: string;
  },
): Promise<ReviewDraft> {
  const finding = findingById(reportData, input.findingId);
  const scope = input.scope ?? exactScopeFromFinding(finding);
  validateReasonForScope(scope, input.verdict, input.reason);

  if (scope.kind === "reviewed-policy") {
    if (!validateReviewedPolicyScope(scope)) {
      throw new ReviewDecisionError("invalid-scope", "reviewed-policy scope is invalid");
    }
    if (scope.justification.trim().length === 0) {
      throw new ReviewDecisionError(
        "invalid-scope",
        "reviewed-policy scope requires justification",
      );
    }
  }

  return withReviewDraft(reportDir, (draft) => {
    const current = stagedEntryFor(draft, input.findingId);
    const prior: UndoStateEntry =
      current === undefined
        ? {}
        : {
            verdict: current.verdict,
            scope: current.scope,
            ...(current.reason === undefined ? {} : { reason: current.reason }),
          };

    let next = pushUndo(draft, input.findingId, prior);
    const entry: StagedDecisionEntry = {
      findingId: input.findingId,
      evidenceFingerprint: finding.evidenceFingerprint,
      verdict: input.verdict,
      scope,
      decidedAt: input.decidedAt,
      ...(input.reason === undefined || input.reason.trim().length === 0
        ? {}
        : { reason: input.reason.trim() }),
    };
    next = upsertDecision(next, entry);
    return next;
  });
}

export async function undoStagedDecision(
  reportDir: string,
  findingId: string,
): Promise<ReviewDraft> {
  return withReviewDraft(reportDir, (draft) => {
    const current = stagedEntryFor(draft, findingId);
    const { draft: popped, prior } = popUndo(draft, findingId);
    if (prior === undefined) {
      throw new ReviewDecisionError("nothing-to-undo", `no undo history for finding ${findingId}`);
    }

    let next = removeDecision(popped, findingId);
    if (prior.verdict !== undefined && prior.scope !== undefined) {
      if (current === undefined) {
        throw new ReviewDecisionError(
          "undo-restore-failed",
          `cannot restore decision for finding ${findingId}`,
        );
      }
      next = upsertDecision(next, {
        findingId,
        evidenceFingerprint: current.evidenceFingerprint,
        verdict: prior.verdict,
        scope: prior.scope,
        decidedAt: current.decidedAt,
        ...(prior.reason === undefined ? {} : { reason: prior.reason }),
      });
    }
    return next;
  });
}

export async function clearStagedDecision(
  reportDir: string,
  findingId: string,
): Promise<ReviewDraft> {
  return withReviewDraft(reportDir, (draft) => {
    const current = stagedEntryFor(draft, findingId);
    if (current === undefined) {
      return draft;
    }
    return removeDecision(
      pushUndo(draft, findingId, {
        verdict: current.verdict,
        scope: current.scope,
        ...(current.reason === undefined ? {} : { reason: current.reason }),
      }),
      findingId,
    );
  });
}

export async function updateFindingNotes(
  reportDir: string,
  findingId: string,
  notes: string,
): Promise<ReviewDraft> {
  return withReviewDraft(reportDir, (draft) => ({
    ...draft,
    notes: {
      ...draft.notes,
      [findingId]: notes,
    },
  }));
}

export async function updateFindingScope(
  reportDir: string,
  reportData: ReportData,
  input: {
    findingId: string;
    scope: DecisionScope;
    reason?: string;
  },
): Promise<ReviewDraft> {
  const finding = findingById(reportData, input.findingId);
  const current = (await withReviewDraft(reportDir, (draft) => draft)).decisions.find(
    (entry) => entry.findingId === input.findingId,
  );

  if (current === undefined) {
    throw new ReviewDecisionError(
      "no-staged-decision",
      `cannot update scope without a staged decision for ${input.findingId}`,
    );
  }

  validateReasonForScope(input.scope, current.verdict, input.reason ?? current.reason);

  return withReviewDraft(reportDir, (draft) => {
    const entry = stagedEntryFor(draft, input.findingId);
    if (entry === undefined) {
      throw new ReviewDecisionError(
        "no-staged-decision",
        `cannot update scope without a staged decision for ${input.findingId}`,
      );
    }

    return upsertDecision(draft, {
      ...entry,
      scope: input.scope,
      evidenceFingerprint: finding.evidenceFingerprint,
      ...(input.reason === undefined
        ? entry.reason === undefined
          ? {}
          : { reason: entry.reason }
        : { reason: input.reason.trim() }),
    });
  });
}

export function stagedDecisionForFinding(
  draft: ReviewDraft,
  findingId: string,
): StagedDecisionEntry | undefined {
  return stagedEntryFor(draft, findingId);
}

export function notesForFinding(draft: ReviewDraft, findingId: string): string {
  return draft.notes[findingId] ?? "";
}

export function canUndoFinding(draft: ReviewDraft, findingId: string): boolean {
  return (draft.undoStacks[findingId]?.length ?? 0) > 0;
}
