// code -> user-facing message (docs/20-FRONTEND.md §3.2: "Keep a code -> message
// map in lib/ so messages are consistent"). Falls back to the ProblemDetails
// title/detail from the server when a code isn't in the map.

interface ApiErrorLike {
  code?: string
  detail?: string
  errors?: Record<string, string[]>
}

/**
 * Generic framework codes. Unlike the domain codes below, these say nothing on their own —
 * the server's `detail` ("A brand named 'bosch' already exists.") is always more useful
 * than "This action conflicts…", so for these the detail wins and the text here is only a
 * last resort.
 */
const GENERIC_CODES = new Set(["validation_error", "not_found", "conflict"])

/**
 * Domain codes whose server `detail` is *more* specific than any fixed copy, so the detail
 * wins and the entry in the map below is only a fallback.
 *
 * The graph validator names the offending stages ("...contains a cycle involving: Cut frame,
 * Assemble"), and the transfer/leftover guards quote the actual figures ("only 20 is
 * available (produced 50 - scrapped 0 - transferred 30)"). Replacing that with generic prose
 * would throw away exactly what the user needs to fix the problem — and, for the graph codes,
 * what the canvas uses to focus the offending node (docs/21-FRONTEND-PHASE2.md §2).
 */
const DETAIL_PREFERRED_CODES = new Set([
  "GRAPH_CYCLE",
  "GRAPH_TERMINAL_COUNT",
  "GRAPH_DISCONNECTED",
  "GRAPH_INPUT_SOURCE_INVALID",
  "TERMINAL_OUTPUT_ITEM_REQUIRED",
  "TRANSFER_EXCEEDS_AVAILABLE",
  "LEFTOVER_EXCEEDS_CONSUMED",
  "REQUIRED_FIELD_MISSING",
])

const CODE_MESSAGES: Record<string, string> = {
  OVER_RECEIPT_TOLERANCE: "This quantity exceeds the purchase order's open quantity beyond the allowed tolerance.",
  STOCK_NEGATIVE_BLOCKED: "Not enough available stock for this action.",
  EXPIRED_BATCH_BLOCKED: "This batch has expired and cannot be issued.",
  ONHOLD_NOT_ISSUABLE: "This stock is on hold/quarantined and cannot be issued.",
  REASON_CODE_REQUIRED: "A reason code is required for this action.",
  PO_NOT_EDITABLE: "This purchase order can no longer be edited.",
  SKU_DUPLICATE: "That SKU already exists.",
  MASTER_IN_USE: "This record is referenced by other data and cannot be deleted.",
  CONCURRENCY_CONFLICT: "This record was changed by someone else. Reload and try again.",
  PRECONDITION_REQUIRED: "This record needs to be reloaded before it can be updated.",
  IDEMPOTENCY_REPLAY: "This request was already processed; showing the original result.",
  EMPLOYEE_CODE_DUPLICATE: "An employee with that code already exists.",
  EMPLOYEE_ALREADY_LINKED: "This staff record already has a linked system user.",
  USER_ALREADY_LINKED: "This user account is already linked to a different staff record.",
  DEPARTMENT_CYCLE_DETECTED: "Setting this parent would create a department cycle.",
  DOCUMENT_TYPE_IN_USE: "This document type is referenced by existing documents and cannot be removed.",
  FILE_TYPE_NOT_ALLOWED: "That file type isn't allowed. Use PDF, JPG, PNG, or DOCX.",
  FILE_TOO_LARGE: "That file is too large.",
  ATTENDANCE_BATCH_LOCKED: "This attendance batch is locked and cannot be edited.",
  ATTENDANCE_DUPLICATE_UNRESOLVED: "Some records have unresolved errors or duplicates.",
  ATTENDANCE_NOT_CONFIRMED: "Attendance for this period must be Confirmed before payroll can be generated.",
  SALARY_STRUCTURE_OVERLAP: "The new effective date must be after the current salary structure's effective date.",
  TAX_SLAB_GAP_INVALID: "This tax slab overlaps another slab for the same effective date.",
  PAYROLL_PERIOD_LOCKED: "This payroll run is locked.",
  // Manufacturing / Production Lines (docs/30-BACKEND-PHASE2.md §D.4). Several of these are
  // listed in DETAIL_PREFERRED_CODES above, so their copy here is only a fallback.
  TEMPLATE_IN_USE: "This template has runs in progress and can't be edited until they finish.",
  TEMPLATE_INACTIVE: "This template is inactive, so new runs can't be started from it.",
  GRAPH_CYCLE: "The stages form a loop. Remove the connection that feeds back on itself.",
  GRAPH_TERMINAL_COUNT: "Connect stages so the line converges to a single final stage.",
  GRAPH_DISCONNECTED: "Every stage must sit on a path from a starting stage to the final stage.",
  GRAPH_INPUT_SOURCE_INVALID: "An upstream input must draw from a stage directly feeding into it.",
  TERMINAL_OUTPUT_ITEM_REQUIRED: "The final stage needs exactly one output, and it must name the finished item.",
  STAGE_NOT_READY: "This stage isn't ready to start yet.",
  STAGE_NOT_IN_PROGRESS: "This stage isn't in progress, so it can't be completed.",
  STAGE_NOT_DONE: "This stage has to be completed before it can be approved.",
  STAGE_NOT_EDITABLE: "Quantities can't be changed once the stage has started.",
  STAGE_REJECT_INVALID: "This stage has no received work to reject.",
  REQUIRED_FIELD_MISSING: "Fill in every required field before completing this stage.",
  TRANSFER_EXCEEDS_AVAILABLE: "That's more than this stage has available to transfer.",
  LEFTOVER_EXCEEDS_CONSUMED: "You can't return more than was consumed and not already returned.",
  RUN_COST_CLOSED: "This run is complete — its costs are closed, so leftovers can't be returned.",
  RUN_NOT_CANCELLABLE: "This run can no longer be cancelled.",
  validation_error: "Please check the highlighted fields.",
  not_found: "The requested record was not found.",
  conflict: "This action conflicts with the record's current state.",
}

export function errorMessage(error: unknown): string {
  if (error && typeof error === "object") {
    const e = error as ApiErrorLike
    // A specific domain code beats the server's prose; a generic one — or one whose detail
    // carries the specifics — loses to it.
    if (
      e.code &&
      !GENERIC_CODES.has(e.code) &&
      !(DETAIL_PREFERRED_CODES.has(e.code) && e.detail) &&
      CODE_MESSAGES[e.code]
    ) {
      return CODE_MESSAGES[e.code]
    }
    if (e.detail) return e.detail
    if (e.code && CODE_MESSAGES[e.code]) return CODE_MESSAGES[e.code]
  }
  if (error instanceof Error) return error.message
  return "Something went wrong."
}

export function fieldErrors(error: unknown): Record<string, string> | null {
  const errors = error && typeof error === "object" ? (error as ApiErrorLike).errors : undefined
  if (!errors) return null
  const out: Record<string, string> = {}
  for (const [field, messages] of Object.entries(errors)) {
    out[field] = messages[0]
  }
  return out
}
