import { Injectable, inject } from '@angular/core';
import { Result, ok, err } from '@shared/kernel/fp';
import { runSubmit } from '@shared/application/submit';
import {
  ApiClient,
  BriefDecisionsDto,
  BriefDto,
  BriefStatusDto,
  BriefViewDto,
  LetterBlockDto,
  LetterSectionDto,
  LibraryPassageDto,
  PlaceholderDefDto,
  RichTextBlockDto,
  RichTextNodeDto,
} from '@shared/infrastructure/api-client';
import {
  Brief,
  BriefDecisions,
  BriefStatus,
  LetterBlock,
  LetterSection,
  LibraryPassage,
} from '@brief/domain/brief';
import { PlaceholderDef } from '@brief/domain/placeholders';
import { Mark, Paragraph, RichTextBlock, RichTextNode } from '@shared/kernel/rich-text';

/**
 * The only place brief HTTP lives (ADR-0001 anti-corruption boundary). The wire
 * uses FLAT unions (a `type`/`tag` string + nullable fields, the repo convention);
 * the `parse*` boundary narrows them into the domain's proper discriminated unions
 * and rejects malformed shapes. Mutations go through `runSubmit` (ProblemDetails →
 * error string), then parse the returned brief.
 */

export interface BriefView {
  readonly brief: Brief;
  readonly availablePassages: LibraryPassage[];
  readonly decisions: BriefDecisions;
}

export const BRIEF_LOAD_FAILED = $localize`:@@brief.load.failed:De brief kon niet worden geladen.`;
export const BRIEF_ACTION_FAILED = $localize`:@@brief.action.failed:De actie is niet gelukt. Probeer het later opnieuw.`;

@Injectable({ providedIn: 'root' })
export class BriefAdapter {
  private client = inject(ApiClient);

  async load(): Promise<Result<string, BriefView>> {
    const r = await runSubmit(() => this.client.briefGET(), BRIEF_LOAD_FAILED);
    return r.ok ? parseBriefView(r.value) : r;
  }

  async save(sections: readonly LetterSection[]): Promise<Result<string, BriefView>> {
    const r = await runSubmit(
      () => this.client.briefPUT({ sections: sections.map(sectionToDto) }),
      BRIEF_ACTION_FAILED,
    );
    return r.ok ? parseBriefView(r.value) : r;
  }

  async submit(): Promise<Result<string, BriefView>> {
    const r = await runSubmit(() => this.client.briefSubmit(), BRIEF_ACTION_FAILED);
    return r.ok ? parseBriefView(r.value) : r;
  }

  async approve(): Promise<Result<string, BriefView>> {
    const r = await runSubmit(() => this.client.approve(), BRIEF_ACTION_FAILED);
    return r.ok ? parseBriefView(r.value) : r;
  }

  async reject(comments: string): Promise<Result<string, BriefView>> {
    const r = await runSubmit(() => this.client.reject({ comments }), BRIEF_ACTION_FAILED);
    return r.ok ? parseBriefView(r.value) : r;
  }

  async send(): Promise<Result<string, BriefView>> {
    const r = await runSubmit(() => this.client.send(), BRIEF_ACTION_FAILED);
    return r.ok ? parseBriefView(r.value) : r;
  }

  /** Demo "start over" — recreate a fresh brief server-side and return the new view. */
  async reset(): Promise<Result<string, BriefView>> {
    const r = await runSubmit(() => this.client.briefReset(), BRIEF_ACTION_FAILED);
    return r.ok ? parseBriefView(r.value) : r;
  }
}

// --- parse: wire (flat) → domain (discriminated unions), validating at the boundary ---

const MARKS: readonly string[] = ['bold', 'italic', 'underline'];

export function parseNode(dto: RichTextNodeDto): Result<string, RichTextNode> {
  switch (dto.type) {
    case 'text': {
      if (typeof dto.text !== 'string') return err('node: text missing text');
      const marks = dto.marks?.filter((m): m is Mark => MARKS.includes(m));
      return ok(
        marks && marks.length
          ? { type: 'text', text: dto.text, marks }
          : { type: 'text', text: dto.text },
      );
    }
    case 'placeholder':
      return typeof dto.key === 'string'
        ? ok({ type: 'placeholder', key: dto.key })
        : err('node: placeholder missing key');
    case 'lineBreak':
      return ok({ type: 'lineBreak' });
    default:
      return err(`node: unknown type ${dto.type}`);
  }
}

export function parseBlockContent(
  dto: RichTextBlockDto | undefined,
): Result<string, RichTextBlock> {
  if (!dto || !Array.isArray(dto.paragraphs)) return err('content: paragraphs not an array');
  const paragraphs: Paragraph[] = [];
  for (const p of dto.paragraphs) {
    const nodes: RichTextNode[] = [];
    for (const n of p.nodes ?? []) {
      const parsed = parseNode(n);
      if (!parsed.ok) return parsed;
      nodes.push(parsed.value);
    }
    const list = p.list === 'bullet' || p.list === 'number' ? p.list : undefined;
    paragraphs.push(list ? { nodes, list } : { nodes });
  }
  return ok({ paragraphs });
}

function parseBlock(dto: LetterBlockDto): Result<string, LetterBlock> {
  if (typeof dto.blockId !== 'string') return err('block: missing blockId');
  const content = parseBlockContent(dto.content);
  if (!content.ok) return content;
  switch (dto.type) {
    case 'passage':
      if (typeof dto.sourcePassageId !== 'string' || typeof dto.sourceVersion !== 'number')
        return err('block: bad passage provenance');
      return ok({
        type: 'passage',
        blockId: dto.blockId,
        sourcePassageId: dto.sourcePassageId,
        sourceVersion: dto.sourceVersion,
        content: content.value,
        edited: dto.edited ?? false,
      });
    case 'freeText':
      return ok({ type: 'freeText', blockId: dto.blockId, content: content.value });
    default:
      return err(`block: unknown type ${dto.type}`);
  }
}

function parseSection(dto: LetterSectionDto): Result<string, LetterSection> {
  if (
    typeof dto.sectionKey !== 'string' ||
    typeof dto.title !== 'string' ||
    typeof dto.required !== 'boolean'
  ) {
    return err('section: bad shape');
  }
  const blocks: LetterBlock[] = [];
  for (const b of dto.blocks ?? []) {
    const parsed = parseBlock(b);
    if (!parsed.ok) return parsed;
    blocks.push(parsed.value);
  }
  return ok({
    sectionKey: dto.sectionKey,
    title: dto.title,
    required: dto.required,
    locked: dto.locked ?? false,
    blocks,
  });
}

function parsePlaceholderDef(dto: PlaceholderDefDto): Result<string, PlaceholderDef> {
  if (
    typeof dto.key !== 'string' ||
    typeof dto.label !== 'string' ||
    typeof dto.autoResolvable !== 'boolean'
  ) {
    return err('placeholder: bad shape');
  }
  return ok({
    key: dto.key,
    label: dto.label,
    autoResolvable: dto.autoResolvable,
    ...(dto.fillable != null ? { fillable: dto.fillable } : {}),
    ...(dto.deprecated != null ? { deprecated: dto.deprecated } : {}),
  });
}

export function parseStatus(dto: BriefStatusDto | undefined): Result<string, BriefStatus> {
  switch (dto?.tag) {
    case 'draft':
      return ok({ tag: 'draft' });
    case 'submitted':
      if (typeof dto.submittedBy !== 'string' || typeof dto.submittedAt !== 'string')
        return err('status: bad submitted');
      return ok({ tag: 'submitted', submittedBy: dto.submittedBy, submittedAt: dto.submittedAt });
    case 'approved':
      if (typeof dto.approvedBy !== 'string' || typeof dto.approvedAt !== 'string')
        return err('status: bad approved');
      return ok({ tag: 'approved', approvedBy: dto.approvedBy, approvedAt: dto.approvedAt });
    case 'rejected':
      if (
        typeof dto.rejectedBy !== 'string' ||
        typeof dto.rejectedAt !== 'string' ||
        typeof dto.comments !== 'string'
      )
        return err('status: bad rejected');
      return ok({
        tag: 'rejected',
        rejectedBy: dto.rejectedBy,
        rejectedAt: dto.rejectedAt,
        comments: dto.comments,
      });
    case 'sent':
      if (typeof dto.sentAt !== 'string') return err('status: bad sent');
      return ok({ tag: 'sent', sentAt: dto.sentAt });
    default:
      return err(`status: unknown tag ${dto?.tag}`);
  }
}

function parsePassage(dto: LibraryPassageDto): Result<string, LibraryPassage> {
  if (typeof dto.passageId !== 'string') return err('passage: bad shape');
  if (dto.scope !== 'global' && dto.scope !== 'beroep')
    return err(`passage: unknown scope ${dto.scope}`);
  if (
    typeof dto.sectionKey !== 'string' ||
    typeof dto.label !== 'string' ||
    typeof dto.version !== 'number'
  )
    return err('passage: bad shape');
  const content = parseBlockContent(dto.content);
  if (!content.ok) return content;
  return ok({
    passageId: dto.passageId,
    scope: dto.scope,
    sectionKey: dto.sectionKey,
    label: dto.label,
    content: content.value,
    version: dto.version,
    ...(dto.beroep != null ? { beroep: dto.beroep } : {}),
  });
}

export function parseBrief(dto: BriefDto): Result<string, Brief> {
  if (
    typeof dto.briefId !== 'string' ||
    typeof dto.drafterId !== 'string' ||
    typeof dto.beroep !== 'string' ||
    typeof dto.templateId !== 'string'
  ) {
    return err('brief: missing ids');
  }
  const status = parseStatus(dto.status);
  if (!status.ok) return status;

  const placeholders: PlaceholderDef[] = [];
  for (const p of dto.placeholders ?? []) {
    const parsed = parsePlaceholderDef(p);
    if (!parsed.ok) return parsed;
    placeholders.push(parsed.value);
  }
  const sections: LetterSection[] = [];
  for (const s of dto.sections ?? []) {
    const parsed = parseSection(s);
    if (!parsed.ok) return parsed;
    sections.push(parsed.value);
  }
  return ok({
    briefId: dto.briefId,
    beroep: dto.beroep,
    templateId: dto.templateId,
    placeholders,
    sections,
    status: status.value,
    drafterId: dto.drafterId,
  });
}

function parseDecisions(dto: BriefDecisionsDto | undefined): Result<string, BriefDecisions> {
  if (
    typeof dto?.canEdit !== 'boolean' ||
    typeof dto.canApprove !== 'boolean' ||
    typeof dto.canReject !== 'boolean' ||
    typeof dto.canSend !== 'boolean'
  ) {
    return err('brief-view: missing/invalid decisions');
  }
  return ok({
    canEdit: dto.canEdit,
    canApprove: dto.canApprove,
    canReject: dto.canReject,
    canSend: dto.canSend,
  });
}

export function parseBriefView(dto: BriefViewDto): Result<string, BriefView> {
  if (!dto.brief) return err('brief-view: missing brief');
  const brief = parseBrief(dto.brief);
  if (!brief.ok) return brief;
  const decisions = parseDecisions(dto.decisions);
  if (!decisions.ok) return decisions;
  const availablePassages: LibraryPassage[] = [];
  for (const p of dto.availablePassages ?? []) {
    const parsed = parsePassage(p);
    if (!parsed.ok) return parsed;
    availablePassages.push(parsed.value);
  }
  return ok({ brief: brief.value, availablePassages, decisions: decisions.value });
}

// --- toDto: domain → wire, for save (collapses the union to the flat shape) ---

function nodeToDto(n: RichTextNode): RichTextNodeDto {
  switch (n.type) {
    case 'text':
      return { type: 'text', text: n.text, ...(n.marks ? { marks: [...n.marks] } : {}) };
    case 'placeholder':
      return { type: 'placeholder', key: n.key };
    case 'lineBreak':
      return { type: 'lineBreak' };
  }
}

function contentToDto(content: RichTextBlock): RichTextBlockDto {
  return {
    paragraphs: content.paragraphs.map((p) => ({
      nodes: p.nodes.map(nodeToDto),
      ...(p.list ? { list: p.list } : {}),
    })),
  };
}

function blockToDto(b: LetterBlock): LetterBlockDto {
  return b.type === 'passage'
    ? {
        type: 'passage',
        blockId: b.blockId,
        content: contentToDto(b.content),
        sourcePassageId: b.sourcePassageId,
        sourceVersion: b.sourceVersion,
        edited: b.edited,
      }
    : { type: 'freeText', blockId: b.blockId, content: contentToDto(b.content) };
}

function sectionToDto(s: LetterSection): LetterSectionDto {
  return {
    sectionKey: s.sectionKey,
    title: s.title,
    required: s.required,
    locked: s.locked,
    blocks: s.blocks.map(blockToDto),
  };
}
