import { assertNever } from '@shared/kernel/fp';
import {
  Brief,
  BriefDecisions,
  BriefStatus,
  LetterBlock,
  LetterSection,
  LibraryPassage,
  allBlocks,
  canSubmit,
} from './brief';
import { RichTextBlock, deepCopyBlock, emptyBlock } from '@shared/kernel/rich-text';

/**
 * The letter composition state machine (Model + Msg + pure reduce), modeled on
 * `herregistratie/domain/intake.machine.ts`.
 *
 * Two invariants are enforced *here*, not in the UI:
 *  - Status transitions are total and guarded — an out-of-order transition Msg is a
 *    no-op (`draft→submitted→approved/rejected→draft`, `approved→sent`).
 *  - Edits are only possible in `draft`/`rejected`; editing a `rejected` letter flips
 *    it back to `draft`. Sections can never be added, removed, or reordered — there
 *    is no Msg for it, so it is unrepresentable.
 *
 * Authorization is NOT a reducer concern: `decisions` (canEdit/canApprove/canReject/
 * canSend) arrives from the server on every load and every status transition (PRD-0002
 * phase P1) and is carried through unchanged by the reducer — never recomputed here.
 * The reducer guards the status invariant; the server is the sole authority on who may
 * act on it.
 *
 * Note: there is no `PlaceholderInserted` Msg. The editor inserts a placeholder NODE
 * at the caret and emits the whole new block via `BlockContentEdited`; its insert menu
 * only offers keys from `brief.placeholders`, so inserting an unknown key is
 * structurally impossible (a pasted `{{…}}` is caught by the linter as `malformed`).
 */

export type BriefState =
  | { tag: 'loading' }
  | {
      tag: 'loaded';
      brief: Brief;
      availablePassages: readonly LibraryPassage[];
      decisions: BriefDecisions;
    }
  | { tag: 'failed'; reason: string };

export const initial: BriefState = { tag: 'loading' };

export type BriefMsg =
  | {
      tag: 'BriefLoaded';
      brief: Brief;
      availablePassages: readonly LibraryPassage[];
      decisions: BriefDecisions;
    }
  | { tag: 'BriefLoadFailed'; reason: string }
  | { tag: 'PassagesInserted'; sectionKey: string; passages: readonly LibraryPassage[] } // multi-select
  | { tag: 'FreeTextBlockAdded'; sectionKey: string }
  | { tag: 'BlockContentEdited'; blockId: string; content: RichTextBlock }
  | { tag: 'BlockRemoved'; blockId: string }
  | { tag: 'BlockMovedWithinSection'; blockId: string; toIndex: number }
  | { tag: 'Submitted'; by: string; at: string; decisions: BriefDecisions } // draft → submitted
  | { tag: 'Approved'; by: string; at: string; decisions: BriefDecisions } // submitted → approved
  | { tag: 'Rejected'; by: string; at: string; comments: string; decisions: BriefDecisions } // submitted → rejected
  | { tag: 'Sent'; at: string; decisions: BriefDecisions } // approved → sent
  | { tag: 'Seed'; state: BriefState };

/** Edits are allowed only in these statuses; editing a rejected letter reopens it. */
function isEditable(status: BriefStatus): boolean {
  return status.tag === 'draft' || status.tag === 'rejected';
}

/** Next `local-N` block id — DERIVED from existing ids (max + 1), not a stored counter. */
function nextLocalIndex(brief: Brief): number {
  let max = 0;
  for (const b of allBlocks(brief)) {
    const m = /^local-(\d+)$/.exec(b.blockId);
    if (m) max = Math.max(max, Number(m[1]));
  }
  return max + 1;
}

function mapSection(
  brief: Brief,
  sectionKey: string,
  f: (s: LetterSection) => LetterSection,
): Brief {
  return {
    ...brief,
    sections: brief.sections.map((s) => (s.sectionKey === sectionKey ? f(s) : s)),
  };
}

/** The section a block currently lives in, or undefined if the block is gone. */
function sectionKeyOfBlock(brief: Brief, blockId: string): string | undefined {
  return brief.sections.find((s) => s.blocks.some((b) => b.blockId === blockId))?.sectionKey;
}

/** A section accepts edits only when it is not a locked (predefined) template section. */
function isSectionEditable(brief: Brief, sectionKey: string | undefined): boolean {
  const section = brief.sections.find((s) => s.sectionKey === sectionKey);
  return !!section && !section.locked;
}

function mapBlocks(brief: Brief, f: (blocks: readonly LetterBlock[]) => LetterBlock[]): Brief {
  return { ...brief, sections: brief.sections.map((s) => ({ ...s, blocks: f(s.blocks) })) };
}

/** Apply an edit to the brief, guarded by status. A rejected letter reopens to draft. */
function withEdit(s: BriefState, f: (b: Brief) => Brief): BriefState {
  if (s.tag !== 'loaded' || !isEditable(s.brief.status)) return s;
  let brief = f(s.brief);
  if (brief.status.tag === 'rejected') brief = { ...brief, status: { tag: 'draft' } };
  return { ...s, brief };
}

function insertPassages(
  brief: Brief,
  sectionKey: string,
  passages: readonly LibraryPassage[],
): Brief {
  let idx = nextLocalIndex(brief);
  // The freeze happens HERE: each block gets a deep VALUE copy of the library content,
  // so later library edits can never mutate this letter (frozen snapshot).
  const newBlocks: LetterBlock[] = passages.map((p) => ({
    type: 'passage',
    blockId: `local-${idx++}`,
    sourcePassageId: p.passageId,
    sourceVersion: p.version,
    content: deepCopyBlock(p.content),
    edited: false,
  }));
  return mapSection(brief, sectionKey, (s) => ({ ...s, blocks: [...s.blocks, ...newBlocks] }));
}

function addFreeText(brief: Brief, sectionKey: string): Brief {
  const block: LetterBlock = {
    type: 'freeText',
    blockId: `local-${nextLocalIndex(brief)}`,
    content: emptyBlock(),
  };
  return mapSection(brief, sectionKey, (s) => ({ ...s, blocks: [...s.blocks, block] }));
}

function editBlockContent(brief: Brief, blockId: string, content: RichTextBlock): Brief {
  return mapBlocks(brief, (blocks) =>
    blocks.map((b) =>
      b.blockId !== blockId
        ? b
        : b.type === 'passage'
          ? { ...b, content, edited: true } // editing a snapshot marks it, keeps provenance
          : { ...b, content },
    ),
  );
}

function moveWithinSection(
  blocks: readonly LetterBlock[],
  blockId: string,
  toIndex: number,
): LetterBlock[] {
  const from = blocks.findIndex((b) => b.blockId === blockId);
  if (from === -1) return [...blocks];
  const clamped = Math.max(0, Math.min(toIndex, blocks.length - 1));
  const next = [...blocks];
  const [moved] = next.splice(from, 1);
  next.splice(clamped, 0, moved);
  return next;
}

export function reduce(s: BriefState, m: BriefMsg): BriefState {
  switch (m.tag) {
    case 'BriefLoaded':
      return {
        tag: 'loaded',
        brief: m.brief,
        availablePassages: m.availablePassages,
        decisions: m.decisions,
      };
    case 'BriefLoadFailed':
      return { tag: 'failed', reason: m.reason };
    case 'Seed':
      return m.state;

    // Section-level guard (defense-in-depth): locked sections never accept edits, even if a
    // Msg reaches the reducer. The UI already hides controls for locked sections.
    case 'PassagesInserted':
      return withEdit(s, (b) =>
        isSectionEditable(b, m.sectionKey) ? insertPassages(b, m.sectionKey, m.passages) : b,
      );
    case 'FreeTextBlockAdded':
      return withEdit(s, (b) =>
        isSectionEditable(b, m.sectionKey) ? addFreeText(b, m.sectionKey) : b,
      );
    case 'BlockContentEdited':
      return withEdit(s, (b) =>
        isSectionEditable(b, sectionKeyOfBlock(b, m.blockId))
          ? editBlockContent(b, m.blockId, m.content)
          : b,
      );
    case 'BlockRemoved':
      return withEdit(s, (b) =>
        isSectionEditable(b, sectionKeyOfBlock(b, m.blockId))
          ? mapBlocks(b, (blocks) => blocks.filter((x) => x.blockId !== m.blockId))
          : b,
      );
    case 'BlockMovedWithinSection':
      return withEdit(s, (b) =>
        isSectionEditable(b, sectionKeyOfBlock(b, m.blockId))
          ? mapBlocks(b, (blocks) =>
              blocks.some((x) => x.blockId === m.blockId)
                ? moveWithinSection(blocks, m.blockId, m.toIndex)
                : [...blocks],
            )
          : b,
      );

    case 'Submitted':
      // Guard the transition AND the completeness invariant.
      return transition(
        s,
        'draft',
        () => ({ tag: 'submitted', submittedBy: m.by, submittedAt: m.at }),
        m.decisions,
        canSubmit,
      );
    case 'Approved':
      return transition(
        s,
        'submitted',
        () => ({ tag: 'approved', approvedBy: m.by, approvedAt: m.at }),
        m.decisions,
      );
    case 'Rejected':
      return transition(
        s,
        'submitted',
        () => ({ tag: 'rejected', rejectedBy: m.by, rejectedAt: m.at, comments: m.comments }),
        m.decisions,
      );
    case 'Sent':
      return transition(s, 'approved', () => ({ tag: 'sent', sentAt: m.at }), m.decisions);

    default:
      return assertNever(m);
  }
}

/** A guarded status transition: only fires from `from`, and only if `guard` passes.
    `decisions` replaces the prior server-computed flags — always fresh from the
    same response that carried the new status. */
function transition(
  s: BriefState,
  from: BriefStatus['tag'],
  next: () => BriefStatus,
  decisions: BriefDecisions,
  guard: (b: Brief) => boolean = () => true,
): BriefState {
  if (s.tag !== 'loaded' || s.brief.status.tag !== from || !guard(s.brief)) return s;
  return { ...s, brief: { ...s.brief, status: next() }, decisions };
}
