import { Result, assertNever } from '@shared/kernel/fp';
import { Uren, parseUren } from '@registratie/domain/value-objects/uren';
import {
  UploadState,
  UploadMsg,
  initialUpload,
  reduceUpload,
  requiredCategoriesSatisfied,
  deliveryRefs,
} from '@shared/upload/upload.machine';

/** What the user is typing (raw, possibly invalid). */
export interface Draft {
  uren: string;
  jaren: string;
  punten: string;
}

export type StepErrors = Partial<Record<keyof Draft | 'documenten', string>>;

/** What we have AFTER parsing — branded/typed, guaranteed valid. */
export interface Valid {
  uren: Uren;
  jaren: number;
  punten: number;
  documents: Array<{ categoryId: string; channel: 'digital' | 'post'; documentId?: string }>;
}

/**
 * The whole wizard as one tagged union. `step` and `errors` exist ONLY while
 * Editing; Submitting/Submitted/Failed carry a `Valid` payload and nothing else.
 * So "submitting while a field is invalid" or "showing the success screen with
 * errors set" are unrepresentable — the bug class is gone by construction.
 */
export type WizardState =
  | { tag: 'Editing'; step: 1 | 2 | 3; draft: Draft; errors: StepErrors; upload: UploadState }
  | { tag: 'Submitting'; data: Valid }
  | { tag: 'Submitted'; data: Valid }
  | { tag: 'Failed'; data: Valid; error: string };

export const initial: WizardState = {
  tag: 'Editing',
  step: 1,
  draft: { uren: '', jaren: '', punten: '' },
  errors: {},
  upload: initialUpload,
};

/** Has the user meaningfully started, so it's worth persisting as a Concept? */
export function hasProgress(s: Extract<WizardState, { tag: 'Editing' }>): boolean {
  return (
    s.step > 1 ||
    !!s.draft.uren ||
    !!s.draft.jaren ||
    !!s.draft.punten ||
    deliveryRefs(s.upload).some((r) => r.channel === 'digital' && !!r.documentId)
  );
}

/** Parse every field; on success hand back a Valid, else the per-field errors. */
function validate(draft: Draft, upload: UploadState): Result<StepErrors, Valid> {
  const uren = parseUren(draft.uren);
  const jaren = parseUren(draft.jaren);
  const punten = parseUren(draft.punten);
  const errors: StepErrors = {};
  if (!uren.ok) errors.uren = uren.error;
  if (!jaren.ok) errors.jaren = jaren.error;
  if (!punten.ok) errors.punten = punten.error;
  if (!requiredCategoriesSatisfied(upload)) {
    errors.documenten = $localize`:@@validation.documenten:Lever de verplichte documenten aan (upload of kies "per post nasturen").`;
  }
  if (uren.ok && jaren.ok && punten.ok && !errors.documenten) {
    return {
      ok: true,
      value: {
        uren: uren.value,
        jaren: jaren.value,
        punten: punten.value,
        documents: deliveryRefs(upload),
      },
    };
  }
  return { ok: false, error: errors };
}

/** Advance one step, gating on that step's fields. Illegal elsewhere = no-op. */
export function next(s: WizardState): WizardState {
  if (s.tag !== 'Editing') return s;
  const errors: StepErrors = {};
  if (s.step === 1) {
    const uren = parseUren(s.draft.uren);
    const jaren = parseUren(s.draft.jaren);
    if (!uren.ok) errors.uren = uren.error;
    if (!jaren.ok) errors.jaren = jaren.error;
    return Object.keys(errors).length === 0 ? { ...s, step: 2, errors: {} } : { ...s, errors };
  }
  if (s.step === 2) {
    const punten = parseUren(s.draft.punten);
    if (!punten.ok) errors.punten = punten.error;
    return punten.ok ? { ...s, step: 3, errors: {} } : { ...s, errors };
  }
  return s;
}

export function back(s: WizardState): WizardState {
  if (s.tag !== 'Editing' || s.step === 1) return s;
  return { ...s, step: (s.step - 1) as 1 | 2, errors: {} };
}

/** Jump back to an earlier step to correct data (controle → step N). Forward
    jumps are not allowed (would skip validation). */
export function gaNaarStap(s: WizardState, step: 1 | 2 | 3): WizardState {
  if (s.tag !== 'Editing' || step >= s.step) return s;
  return { ...s, step, errors: {} };
}

/** Step 3 submit: parse everything + require documents; Submitting only with Valid. */
export function submit(s: WizardState): WizardState {
  if (s.tag !== 'Editing' || s.step !== 3) return s;
  const result = validate(s.draft, s.upload);
  return result.ok ? { tag: 'Submitting', data: result.value } : { ...s, errors: result.error };
}

/** Route an upload sub-message through the pure upload reducer (Editing only). */
export function upload(s: WizardState, msg: UploadMsg): WizardState {
  if (s.tag !== 'Editing') return s;
  return { ...s, upload: reduceUpload(s.upload, msg) };
}

/** Resolve the async submit. Only meaningful while Submitting. */
export function resolve(s: WizardState, r: Result<string, void>): WizardState {
  if (s.tag !== 'Submitting') return s;
  return r.ok
    ? { tag: 'Submitted', data: s.data }
    : { tag: 'Failed', data: s.data, error: r.error };
}

/** Update one draft field while editing; ignored in any other state. */
export function setField(s: WizardState, key: keyof Draft, value: string): WizardState {
  if (s.tag !== 'Editing') return s;
  return { ...s, draft: { ...s.draft, [key]: value } };
}

/**
 * Every event that can happen to the wizard, as one message type. The component
 * sends a WizardMsg; `reduce` decides the next state. This is the Elm
 * Model+Msg+update pattern: ONE pure function describes all state changes.
 */
export type WizardMsg =
  | { tag: 'SetField'; key: keyof Draft; value: string }
  | { tag: 'Next' }
  | { tag: 'Back' }
  | { tag: 'GaNaarStap'; step: 1 | 2 | 3 }
  | { tag: 'Submit' }
  | { tag: 'Retry' }
  | { tag: 'SubmitConfirmed' }
  | { tag: 'SubmitFailed'; error: string }
  | { tag: 'Upload'; msg: UploadMsg }
  | { tag: 'Seed'; state: WizardState }; // mount a specific state (stories/showcase)

export function reduce(s: WizardState, m: WizardMsg): WizardState {
  switch (m.tag) {
    case 'SetField':
      return setField(s, m.key, m.value);
    case 'Next':
      return next(s);
    case 'Back':
      return back(s);
    case 'GaNaarStap':
      return gaNaarStap(s, m.step);
    case 'Submit':
      return submit(s);
    case 'Retry':
      return s.tag === 'Failed' ? { tag: 'Submitting', data: s.data } : s;
    case 'SubmitConfirmed':
      return s.tag === 'Submitting' ? { tag: 'Submitted', data: s.data } : s;
    case 'SubmitFailed':
      return s.tag === 'Submitting' ? { tag: 'Failed', data: s.data, error: m.error } : s;
    case 'Upload':
      return upload(s, m.msg);
    case 'Seed':
      return m.state;
    default:
      return assertNever(m);
  }
}
