export type DecisionDocumentType = "DCSN" | "GOAL" | "PLAN" | "RPRT" | "ACTN" | "ISSU" | "RETR" | "RQST" | "RESP" | "MINS";
export type DecisionDocumentPriority = "P0" | "P1" | "P2" | "P3";

export interface DecisionDocumentNumber {
  docNo: string;
  docType: DecisionDocumentType;
  docPriority: DecisionDocumentPriority;
  docYear: number;
  docSeq: number;
}

export interface LegacyDocumentSource {
  id?: string;
  title?: string;
  body?: string;
  tags?: unknown;
}

export class DocumentContractError extends Error {
  readonly detail: Record<string, string | number | boolean | null | string[]>;

  constructor(message: string, detail: Record<string, string | number | boolean | null | string[]> = {}) {
    super(message);
    this.name = "DocumentContractError";
    this.detail = detail;
  }
}

export const decisionDocumentTypes: readonly DecisionDocumentType[] = ["DCSN", "GOAL", "PLAN", "RPRT", "ACTN", "ISSU", "RETR", "RQST", "RESP", "MINS"];
export const decisionDocumentPriorities: readonly DecisionDocumentPriority[] = ["P0", "P1", "P2", "P3"];

export const legacyDocumentNumberMap: Readonly<Record<string, string>> = {
  dc_decision_thesis_unidesk_integration_rule: "DC-DCSN-P0-2026-001",
  dc_goal_big_paper_submission: "DC-GOAL-P0-2026-001",
  dc_goal_small_paper_submission_gate: "DC-GOAL-P0-2026-002",
};

const documentTypeValues = new Set<string>(decisionDocumentTypes);
const documentPriorityValues = new Set<string>(decisionDocumentPriorities);
const docNoSearchPattern = /\bDC-(DCSN|GOAL|PLAN|RPRT|ACTN|ISSU|RETR|RQST|RESP|MINS)-(P[0-3])-(\d{4})-(\d{1,9})\b/iu;

function textValue(value: unknown): string {
  if (value === null || value === undefined) return "";
  if (typeof value === "string") return value.trim();
  if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value).trim();
  return "";
}

function legacyKey(value: string): string {
  return value.trim().toLowerCase();
}

export function buildDocumentNumber(docType: DecisionDocumentType, docPriority: DecisionDocumentPriority, docYear: number, docSeq: number): string {
  return `DC-${docType}-${docPriority}-${docYear}-${String(docSeq).padStart(3, "0")}`;
}

export function parseDocumentNumber(value: unknown, field = "docNo"): DecisionDocumentNumber {
  const raw = textValue(value).toUpperCase();
  const match = raw.match(/^DC-(DCSN|GOAL|PLAN|RPRT|ACTN|ISSU|RETR|RQST|RESP|MINS)-(P[0-3])-(\d{4})-(\d{1,9})$/u);
  if (match === null) {
    throw new DocumentContractError(`${field} must match DC-<TYPE>-<PRIORITY>-<YEAR>-<SEQ>`, {
      field,
      value: textValue(value),
      allowedTypes: [...decisionDocumentTypes],
      allowedPriorities: [...decisionDocumentPriorities],
      example: "DC-GOAL-P0-2026-001",
    });
  }
  const docSeq = Number(match[4]);
  const docYear = Number(match[3]);
  if (!Number.isInteger(docSeq) || docSeq <= 0) {
    throw new DocumentContractError(`${field} sequence must be a positive integer`, { field, value: textValue(value) });
  }
  return {
    docNo: buildDocumentNumber(match[1] as DecisionDocumentType, match[2] as DecisionDocumentPriority, docYear, docSeq),
    docType: match[1] as DecisionDocumentType,
    docPriority: match[2] as DecisionDocumentPriority,
    docYear,
    docSeq,
  };
}

export function tryParseDocumentNumber(value: unknown): DecisionDocumentNumber | null {
  try {
    return parseDocumentNumber(value);
  } catch {
    return null;
  }
}

export function parseDocumentType(value: unknown, field = "docType"): DecisionDocumentType {
  const raw = textValue(value).toUpperCase();
  if (!documentTypeValues.has(raw)) {
    throw new DocumentContractError(`${field} must be one of: ${decisionDocumentTypes.join(", ")}`, {
      field,
      value: textValue(value),
      allowed: [...decisionDocumentTypes],
    });
  }
  return raw as DecisionDocumentType;
}

export function parseOptionalDocumentType(value: unknown, field = "docType"): DecisionDocumentType | "" {
  const raw = textValue(value);
  return raw ? parseDocumentType(raw, field) : "";
}

export function parseDocumentPriority(value: unknown, field = "docPriority"): DecisionDocumentPriority {
  const raw = textValue(value).toUpperCase();
  if (!documentPriorityValues.has(raw)) {
    throw new DocumentContractError(`${field} must be one of: ${decisionDocumentPriorities.join(", ")}`, {
      field,
      value: textValue(value),
      allowed: [...decisionDocumentPriorities],
    });
  }
  return raw as DecisionDocumentPriority;
}

export function parseOptionalDocumentPriority(value: unknown, field = "docPriority"): DecisionDocumentPriority | "" {
  const raw = textValue(value);
  return raw ? parseDocumentPriority(raw, field) : "";
}

export function parseDocumentYear(value: unknown, field = "docYear"): number {
  const raw = textValue(value);
  const year = Number(raw);
  if (!/^\d{4}$/u.test(raw) || !Number.isInteger(year) || year < 1970 || year > 2100) {
    throw new DocumentContractError(`${field} must be a four-digit year between 1970 and 2100`, { field, value: raw });
  }
  return year;
}

export function parseDocumentSequence(value: unknown, field = "docSeq"): number {
  const raw = textValue(value);
  const seq = Number(raw);
  if (!/^\d+$/u.test(raw) || !Number.isInteger(seq) || seq <= 0 || seq > 999_999_999) {
    throw new DocumentContractError(`${field} must be a positive integer`, { field, value: raw });
  }
  return seq;
}

export function parseIssuedAt(value: unknown, field = "issuedAt"): string {
  const raw = textValue(value);
  if (!raw) return "";
  const date = /^\d{4}-\d{2}-\d{2}$/u.test(raw) ? new Date(`${raw}T00:00:00.000Z`) : new Date(raw);
  if (Number.isNaN(date.getTime())) {
    throw new DocumentContractError(`${field} must be an ISO timestamp or YYYY-MM-DD date`, { field, value: raw });
  }
  return date.toISOString();
}

export function normalizeDocumentNumberList(value: unknown, field: string): string[] {
  if (value === null || value === undefined || value === "") return [];
  const rawItems = typeof value === "string"
    ? value.split(",")
    : Array.isArray(value)
      ? value
      : null;
  if (rawItems === null) throw new DocumentContractError(`${field} must be a document number or array of document numbers`, { field });
  const normalized = rawItems
    .map((item) => textValue(item))
    .filter(Boolean)
    .map((item) => parseDocumentNumber(item, field).docNo);
  return [...new Set(normalized)].slice(0, 100);
}

function docNoFromText(text: string, options: { prefixOnly?: boolean } = {}): DecisionDocumentNumber | null {
  const trimmed = text.trim();
  if (!trimmed) return null;
  const docNoTagged = trimmed.match(/^doc-no\s*:\s*(DC-[A-Z0-9-]+)/iu);
  if (docNoTagged !== null) return tryParseDocumentNumber(docNoTagged[1] ?? "");
  const prefix = trimmed.match(/^\[?\s*(DC-[A-Z0-9-]+)\s*\]?/iu);
  if (prefix !== null) return tryParseDocumentNumber(prefix[1] ?? "");
  if (options.prefixOnly === true) return null;
  const match = trimmed.match(docNoSearchPattern);
  return match === null ? null : tryParseDocumentNumber(match[0]);
}

function mappedLegacyDocNo(value: string): DecisionDocumentNumber | null {
  const mapped = legacyDocumentNumberMap[legacyKey(value)];
  return mapped === undefined ? null : parseDocumentNumber(mapped);
}

function firstParagraph(body: string): string {
  return body
    .replace(/\r\n?/gu, "\n")
    .split(/\n\s*\n/gu)
    .map((part) => part.trim())
    .find(Boolean) ?? "";
}

export function extractDocumentNumberFromLegacy(source: LegacyDocumentSource): DecisionDocumentNumber | null {
  const id = textValue(source.id);
  const idMapped = mappedLegacyDocNo(id);
  if (idMapped !== null) return idMapped;

  const tags = Array.isArray(source.tags) ? source.tags.map((tag) => textValue(tag)).filter(Boolean) : [];
  for (const tag of tags) {
    const mapped = mappedLegacyDocNo(tag);
    if (mapped !== null) return mapped;
    const tagged = tag.match(/^doc-no\s*:\s*(.+)$/iu);
    if (tagged !== null) {
      const parsed = tryParseDocumentNumber(tagged[1] ?? "");
      if (parsed !== null) return parsed;
    }
    const parsed = docNoFromText(tag);
    if (parsed !== null) return parsed;
  }

  const title = textValue(source.title);
  const titleMapped = mappedLegacyDocNo(title.split(/\s+/u)[0] ?? "");
  if (titleMapped !== null) return titleMapped;
  const titleDocNo = docNoFromText(title, { prefixOnly: true });
  if (titleDocNo !== null) return titleDocNo;

  const paragraph = firstParagraph(textValue(source.body));
  const paragraphMapped = Object.entries(legacyDocumentNumberMap).find(([legacy]) => paragraph.toLowerCase().includes(legacy));
  if (paragraphMapped !== undefined) return parseDocumentNumber(paragraphMapped[1]);
  return docNoFromText(paragraph);
}
