import { validateProject } from "../../core/validation.js";
import { ProjectState } from "../../core/project-state.js";
import {
  withProjectLock,
  writeIssueUnlocked,
  deleteIssue,
} from "../../core/project-loader.js";
import { nextIssueID } from "../../core/id-allocation.js";
import {
  formatIssueList,
  formatIssue,
  formatError,
  successEnvelope,
  ExitCode,
} from "../../core/output-formatter.js";
import {
  ISSUE_STATUSES,
  ISSUE_SEVERITIES,
  type IssueStatus,
  type IssueSeverity,
} from "../../models/types.js";
import type { Issue } from "../../models/issue.js";
import {
  todayISO,
  normalizeArrayOption,
  CliValidationError,
} from "../helpers.js";
import type { CommandContext, CommandResult } from "../types.js";
import {
  formatMetadataValue,
  getMetadata,
  setMetadata,
  unsetMetadata,
} from "./metadata.js";

// Re-export for register.ts
export { ISSUE_STATUSES, ISSUE_SEVERITIES };

const ISSUE_CORE_METADATA_KEYS = new Set([
  "id",
  "title",
  "status",
  "severity",
  "components",
  "impact",
  "resolution",
  "location",
  "discoveredDate",
  "resolvedDate",
  "relatedTickets",
  "order",
  "phase",
  "createdBy",
  "assignedTo",
  "lastModifiedBy",
]);

// --- Read Handlers ---

export function handleIssueList(
  filters: { status?: string; severity?: string; component?: string },
  ctx: CommandContext,
): CommandResult {
  let issues = [...ctx.state.issues];

  if (filters.status) {
    if (!ISSUE_STATUSES.includes(filters.status as IssueStatus)) {
      throw new CliValidationError(
        "invalid_input",
        `Unknown issue status "${filters.status}": must be one of ${ISSUE_STATUSES.join(", ")}`,
      );
    }
    issues = issues.filter((i) => i.status === filters.status);
  }
  if (filters.severity) {
    if (!ISSUE_SEVERITIES.includes(filters.severity as IssueSeverity)) {
      throw new CliValidationError(
        "invalid_input",
        `Unknown issue severity "${filters.severity}": must be one of ${ISSUE_SEVERITIES.join(", ")}`,
      );
    }
    issues = issues.filter((i) => i.severity === filters.severity);
  }
  if (filters.component) {
    issues = issues.filter((i) => i.components.includes(filters.component!));
  }

  return { output: formatIssueList(issues, ctx.format) };
}

export function handleIssueGet(
  id: string,
  ctx: CommandContext,
): CommandResult {
  const issue = ctx.state.issueByID(id);
  if (!issue) {
    return {
      output: formatError("not_found", `Issue ${id} not found`, ctx.format),
      exitCode: ExitCode.USER_ERROR,
      errorCode: "not_found",
    };
  }
  return { output: formatIssue(issue, ctx.format) };
}

export function handleIssueMetaGet(
  id: string,
  path: string | undefined,
  ctx: CommandContext,
): CommandResult {
  const issue = ctx.state.issueByID(id);
  if (!issue) {
    return {
      output: formatError("not_found", `Issue ${id} not found`, ctx.format),
      exitCode: ExitCode.USER_ERROR,
      errorCode: "not_found",
    };
  }
  const result = getMetadata(issue as Record<string, unknown>, path, ISSUE_CORE_METADATA_KEYS);
  if (!result.found) {
    return {
      output: formatError("not_found", `Metadata path "${path}" not found on issue ${id}`, ctx.format),
      exitCode: ExitCode.USER_ERROR,
      errorCode: "not_found",
    };
  }
  return { output: formatMetadataValue(result.value, ctx.format) };
}

// --- Write Handlers ---

function validateRelatedTickets(ids: string[], state: ProjectState): void {
  for (const tid of ids) {
    if (!state.ticketByID(tid)) {
      throw new CliValidationError("invalid_input", `Related ticket ${tid} not found`);
    }
  }
}

/** Build a multiset of error findings keyed by code|entity|message, with message lookup. */
function buildErrorMultiset(findings: readonly { level: string; code: string; entity: string | null; message: string }[]): { counts: Map<string, number>; messages: Map<string, string> } {
  const counts = new Map<string, number>();
  const messages = new Map<string, string>();
  for (const f of findings) {
    if (f.level !== "error") continue;
    const key = `${f.code}|${f.entity ?? ""}|${f.message}`;
    counts.set(key, (counts.get(key) ?? 0) + 1);
    messages.set(key, f.message);
  }
  return { counts, messages };
}

/** ISS-065: Only block writes that make the project WORSE. Pre-existing errors pass through. */
function validatePostWriteIssueState(
  candidate: Issue,
  state: ProjectState,
  isCreate: boolean,
): void {
  // Pre-write validation
  const preResult = validateProject(state);
  const { counts: preErrors } = buildErrorMultiset(preResult.findings);

  // Post-write validation
  const existingIssues = [...state.issues];
  if (isCreate) {
    existingIssues.push(candidate);
  } else {
    const idx = existingIssues.findIndex((i) => i.id === candidate.id);
    if (idx >= 0) existingIssues[idx] = candidate;
    else existingIssues.push(candidate);
  }
  const postState = new ProjectState({
    tickets: [...state.tickets],
    issues: existingIssues,
    notes: [...state.notes],
    roadmap: state.roadmap,
    config: state.config,
    handoverFilenames: [...state.handoverFilenames],
  });
  const postResult = validateProject(postState);
  const { counts: postErrors, messages: postMessages } = buildErrorMultiset(postResult.findings);

  // Block only if new errors were introduced
  const newErrors: string[] = [];
  for (const [key, postCount] of postErrors) {
    const preCount = preErrors.get(key) ?? 0;
    if (postCount > preCount) {
      newErrors.push(postMessages.get(key) ?? key);
    }
  }
  if (newErrors.length > 0) {
    throw new CliValidationError("validation_failed", `Write would create invalid state: ${newErrors.join("; ")}`);
  }
}

export async function handleIssueCreate(
  args: {
    title: string;
    severity: string;
    impact: string;
    components: string[];
    relatedTickets: string[];
    location: string[];
    phase?: string;
  },
  format: string,
  root: string,
): Promise<CommandResult> {
  if (!ISSUE_SEVERITIES.includes(args.severity as IssueSeverity)) {
    throw new CliValidationError(
      "invalid_input",
      `Unknown issue severity "${args.severity}": must be one of ${ISSUE_SEVERITIES.join(", ")}`,
    );
  }

  let createdIssue: Issue | undefined;

  await withProjectLock(root, { strict: true }, async ({ state }) => {
    if (args.phase && !state.roadmap.phases.some((p) => p.id === args.phase)) {
      throw new CliValidationError("invalid_input", `Phase "${args.phase}" not found in roadmap`);
    }
    if (args.relatedTickets.length > 0) {
      validateRelatedTickets(args.relatedTickets, state);
    }

    const id = nextIssueID(state.issues);
    const issue: Issue = {
      id,
      title: args.title,
      status: "open",
      severity: args.severity as IssueSeverity,
      components: args.components,
      impact: args.impact,
      resolution: null,
      location: args.location,
      discoveredDate: todayISO(),
      resolvedDate: null,
      relatedTickets: args.relatedTickets,
      phase: args.phase ?? null,
    };

    validatePostWriteIssueState(issue, state, true);
    await writeIssueUnlocked(issue, root);
    createdIssue = issue;
  });

  if (!createdIssue) throw new Error("Issue not created");
  if (format === "json") {
    return { output: JSON.stringify(successEnvelope(createdIssue), null, 2) };
  }
  return { output: `Created issue ${createdIssue.id}: ${createdIssue.title}` };
}

export async function handleIssueUpdate(
  id: string,
  updates: {
    status?: string;
    title?: string;
    severity?: string;
    impact?: string;
    resolution?: string | null;
    components?: string[];
    relatedTickets?: string[];
    location?: string[];
    order?: number;
    phase?: string | null;
  },
  format: string,
  root: string,
): Promise<CommandResult> {
  if (updates.status && !ISSUE_STATUSES.includes(updates.status as IssueStatus)) {
    throw new CliValidationError(
      "invalid_input",
      `Unknown issue status "${updates.status}": must be one of ${ISSUE_STATUSES.join(", ")}`,
    );
  }
  if (updates.severity && !ISSUE_SEVERITIES.includes(updates.severity as IssueSeverity)) {
    throw new CliValidationError(
      "invalid_input",
      `Unknown issue severity "${updates.severity}": must be one of ${ISSUE_SEVERITIES.join(", ")}`,
    );
  }

  let updatedIssue: Issue | undefined;

  await withProjectLock(root, { strict: true }, async ({ state }) => {
    const existing = state.issueByID(id);
    if (!existing) {
      throw new CliValidationError("not_found", `Issue ${id} not found`);
    }

    if (updates.phase !== undefined && updates.phase !== null) {
      if (!state.roadmap.phases.some((p) => p.id === updates.phase)) {
        throw new CliValidationError("invalid_input", `Phase "${updates.phase}" not found in roadmap`);
      }
    }
    if (updates.relatedTickets) {
      validateRelatedTickets(updates.relatedTickets, state);
    }

    // Status transition with date management
    const statusChanges: Partial<Issue> = {};
    if (updates.status !== undefined && updates.status !== existing.status) {
      statusChanges.status = updates.status as IssueStatus;
      if (updates.status === "resolved" && existing.status !== "resolved") {
        statusChanges.resolvedDate = todayISO();
      } else if (updates.status !== "resolved" && existing.status === "resolved") {
        statusChanges.resolvedDate = null;
      }
    }

    const issue: Issue = {
      ...existing,
      ...(updates.title !== undefined && { title: updates.title }),
      ...(updates.severity !== undefined && { severity: updates.severity as IssueSeverity }),
      ...(updates.impact !== undefined && { impact: updates.impact }),
      ...(updates.resolution !== undefined && { resolution: updates.resolution }),
      ...(updates.components !== undefined && { components: updates.components }),
      ...(updates.relatedTickets !== undefined && { relatedTickets: updates.relatedTickets }),
      ...(updates.location !== undefined && { location: updates.location }),
      ...(updates.order !== undefined && { order: updates.order }),
      ...(updates.phase !== undefined && { phase: updates.phase }),
      ...statusChanges,
    };

    validatePostWriteIssueState(issue, state, false);
    await writeIssueUnlocked(issue, root);
    updatedIssue = issue;
  });

  if (!updatedIssue) throw new Error("Issue not updated");
  if (format === "json") {
    return { output: JSON.stringify(successEnvelope(updatedIssue), null, 2) };
  }
  return { output: `Updated issue ${updatedIssue.id}: ${updatedIssue.title}` };
}

export async function handleIssueMetaSet(
  id: string,
  path: string,
  value: unknown,
  format: string,
  root: string,
): Promise<CommandResult> {
  let updatedIssue: Issue | undefined;

  await withProjectLock(root, { strict: true }, async ({ state }) => {
    const existing = state.issueByID(id);
    if (!existing) {
      throw new CliValidationError("not_found", `Issue ${id} not found`);
    }
    const issue = setMetadata(
      existing as Record<string, unknown>,
      path,
      value,
      ISSUE_CORE_METADATA_KEYS,
    ) as Issue;
    validatePostWriteIssueState(issue, state, false);
    await writeIssueUnlocked(issue, root);
    updatedIssue = issue;
  });

  if (!updatedIssue) throw new Error("Issue metadata not updated");
  if (format === "json") {
    return { output: JSON.stringify(successEnvelope(updatedIssue), null, 2) };
  }
  return { output: `Updated metadata ${path} on issue ${updatedIssue.id}` };
}

export async function handleIssueMetaUnset(
  id: string,
  path: string,
  format: string,
  root: string,
): Promise<CommandResult> {
  let updatedIssue: Issue | undefined;

  await withProjectLock(root, { strict: true }, async ({ state }) => {
    const existing = state.issueByID(id);
    if (!existing) {
      throw new CliValidationError("not_found", `Issue ${id} not found`);
    }
    const issue = unsetMetadata(
      existing as Record<string, unknown>,
      path,
      ISSUE_CORE_METADATA_KEYS,
    ) as Issue;
    validatePostWriteIssueState(issue, state, false);
    await writeIssueUnlocked(issue, root);
    updatedIssue = issue;
  });

  if (!updatedIssue) throw new Error("Issue metadata not updated");
  if (format === "json") {
    return { output: JSON.stringify(successEnvelope(updatedIssue), null, 2) };
  }
  return { output: `Unset metadata ${path} on issue ${updatedIssue.id}` };
}

export async function handleIssueDelete(
  id: string,
  format: string,
  root: string,
): Promise<CommandResult> {
  await deleteIssue(id, root);
  if (format === "json") {
    return { output: JSON.stringify(successEnvelope({ id, deleted: true }), null, 2) };
  }
  return { output: `Deleted issue ${id}.` };
}
