import { Result, ok, err, assertNever } from '@shared/kernel/fp';
import { Uren, parseUren } from '@registratie/domain/value-objects/uren';

/**
 * A FIXED 3-step wizard with progressive disclosure. The steps never change in
 * number (always `STEPS`); instead, follow-up questions appear *inline within a
 * step* depending on earlier answers — answer "buiten Nederland gewerkt? → ja"
 * and the country/hours questions reveal in the same step; report few hours and
 * the scholing-question reveals inside the 'werk' step. "Is this field required
 * right now" is a pure function (`validateStep`/`lageUren`), so it's trivial to
 * test and impossible to get out of sync with the data.
 */

export type JaNee = 'ja' | 'nee';

/** The three fixed steps. Each step groups one or more questions. */
export type StepId = 'buitenland' | 'werk' | 'review';

/** One record carried across every step (and persisted). All optional: the user
    fills it in gradually, and branches may never ask some fields. */
export interface Answers {
  buitenlandGewerkt?: JaNee; // Q1
  land?: string; // Q1a — only when buitenlandGewerkt === 'ja'
  buitenlandseUren?: string; // Q1b — only when buitenlandGewerkt === 'ja'
  uren?: string; // Q2 — uren in NL
  scholingGevolgd?: JaNee; // Q3 — only when total hours are below the threshold
  punten?: string; // Q4
}

/** What we have after the review step parses — guaranteed valid/typed. */
export interface ValidIntake {
  werktBuitenland: boolean;
  land?: string;
  buitenlandseUren?: Uren;
  uren: Uren;
  aanvullendeScholing?: boolean;
  punten?: Uren; // only collected when aanvullende scholing is gevolgd (scholingGevolgd === 'ja')
}

/** Demo fallback only — the real threshold is a SERVER-OWNED policy value fetched
    at runtime (see IntakePolicyDto / SetPolicy). ponytail: default is the offline
    fallback; the server value wins. */
export const SCHOLING_THRESHOLD_DEFAULT = 1000;

/** The server-owned intake policy (domain-side, parsed from the wire at the boundary). */
export interface IntakePolicy {
  readonly scholingThreshold: number;
}

/** True when NL-hours are low enough that the scholing question must be answered.
    The threshold is passed in (server-owned), not hardcoded. */
export function lageUren(a: Answers, scholingThreshold = SCHOLING_THRESHOLD_DEFAULT): boolean {
  const r = parseUren(a.uren ?? '');
  return r.ok && r.value < scholingThreshold;
}

/** The fixed step list. Number of steps never changes; questions reveal inline. */
export const STEPS: StepId[] = ['buitenland', 'werk', 'review'];

/** Per-field error map: one message per question, since a step holds several. */
type Errors = Partial<Record<keyof Answers, string>>;

export type IntakeState =
  | {
      tag: 'Answering';
      answers: Answers;
      cursor: number;
      errors: Errors;
      scholingThreshold: number;
    }
  | { tag: 'Submitting'; data: ValidIntake }
  | { tag: 'Submitted'; data: ValidIntake }
  | { tag: 'Failed'; data: ValidIntake; error: string };

export const initial: IntakeState = {
  tag: 'Answering',
  answers: {},
  cursor: 0,
  errors: {},
  scholingThreshold: SCHOLING_THRESHOLD_DEFAULT,
};

/** Which step the cursor currently points at (clamped to the fixed list). */
export function currentStep(s: Extract<IntakeState, { tag: 'Answering' }>): StepId {
  return STEPS[Math.min(s.cursor, STEPS.length - 1)];
}

/** Has the user meaningfully started, so it's worth persisting as a Concept?
    (No auto-prefill here — pristine means truly untouched.) */
export function hasProgress(s: Extract<IntakeState, { tag: 'Answering' }>): boolean {
  return s.cursor > 0 || Object.keys(s.answers).length > 0;
}

/** Validate every question currently visible in ONE step. Errors keyed per field. */
function validateStep(step: StepId, a: Answers, scholingThreshold: number): Result<Errors, void> {
  const errors: Errors = {};
  switch (step) {
    case 'buitenland':
      if (!a.buitenlandGewerkt)
        errors.buitenlandGewerkt = $localize`:@@validation.maakKeuze:Maak een keuze.`;
      else if (a.buitenlandGewerkt === 'ja') {
        if (!a.land || a.land.trim() === '')
          errors.land = $localize`:@@validation.land:Vul een land in.`;
        const u = parseUren(a.buitenlandseUren ?? '');
        if (!u.ok) errors.buitenlandseUren = u.error;
      }
      break;
    case 'werk': {
      const u = parseUren(a.uren ?? '');
      if (!u.ok) errors.uren = u.error;
      if (lageUren(a, scholingThreshold) && !a.scholingGevolgd)
        errors.scholingGevolgd = $localize`:@@validation.maakKeuze:Maak een keuze.`;
      // Nascholingspunten are only asked (and required) when scholing was followed.
      if (a.scholingGevolgd === 'ja') {
        const p = parseUren(a.punten ?? '');
        if (!p.ok) errors.punten = p.error;
      }
      break;
    }
    case 'review':
      break; // review shows a summary; no own fields
    default:
      return assertNever(step);
  }
  return Object.keys(errors).length > 0 ? err(errors) : ok(undefined);
}

/** Parse the whole questionnaire into a ValidIntake (called on submit). */
function validateAll(a: Answers, scholingThreshold: number): Result<Errors, ValidIntake> {
  const errors: Errors = {};
  for (const step of STEPS) {
    const r = validateStep(step, a, scholingThreshold);
    if (!r.ok) Object.assign(errors, r.error);
  }
  if (Object.keys(errors).length > 0) return err(errors);

  const uren = parseUren(a.uren ?? '');
  // validateStep guaranteed uren parses, but keep the compiler happy.
  if (!uren.ok) return err(errors);

  const werktBuitenland = a.buitenlandGewerkt === 'ja';
  const buitenland = parseUren(a.buitenlandseUren ?? '');
  // Punten are only collected when aanvullende scholing was gevolgd.
  const punten = a.scholingGevolgd === 'ja' ? parseUren(a.punten ?? '') : undefined;
  return ok({
    werktBuitenland,
    land: werktBuitenland ? a.land : undefined,
    buitenlandseUren: werktBuitenland && buitenland.ok ? buitenland.value : undefined,
    uren: uren.value,
    aanvullendeScholing: lageUren(a, scholingThreshold) ? a.scholingGevolgd === 'ja' : undefined,
    punten: punten?.ok ? punten.value : undefined,
  });
}

export function setAnswer(s: IntakeState, key: keyof Answers, value: string): IntakeState {
  if (s.tag !== 'Answering') return s;
  // Steps are fixed, so editing an answer never moves the cursor — it only
  // reveals/hides inline questions within the current step.
  return { ...s, answers: { ...s.answers, [key]: value } };
}

export function next(s: IntakeState): IntakeState {
  if (s.tag !== 'Answering') return s;
  const r = validateStep(currentStep(s), s.answers, s.scholingThreshold);
  if (!r.ok) return { ...s, errors: r.error };
  return { ...s, cursor: Math.min(s.cursor + 1, STEPS.length - 1), errors: {} };
}

/** Apply a server-owned policy value (e.g. the scholing threshold). */
export function setPolicy(s: IntakeState, scholingThreshold: number): IntakeState {
  return s.tag === 'Answering' ? { ...s, scholingThreshold } : s;
}

export function back(s: IntakeState): IntakeState {
  if (s.tag !== 'Answering' || s.cursor === 0) return s;
  return { ...s, cursor: s.cursor - 1, errors: {} };
}

/** Jump back to an earlier step to correct answers (review → step N). Forward
    jumps are not allowed (would skip validation). */
export function gaNaarStap(s: IntakeState, cursor: number): IntakeState {
  if (s.tag !== 'Answering' || cursor < 0 || cursor >= s.cursor) return s;
  return { ...s, cursor, errors: {} };
}

export function submit(s: IntakeState): IntakeState {
  if (s.tag !== 'Answering') return s;
  const r = validateAll(s.answers, s.scholingThreshold);
  return r.ok ? { tag: 'Submitting', data: r.value } : { ...s, errors: r.error };
}

export function resolve(s: IntakeState, r: Result<string, void>): IntakeState {
  if (s.tag !== 'Submitting') return s;
  return r.ok
    ? { tag: 'Submitted', data: s.data }
    : { tag: 'Failed', data: s.data, error: r.error };
}

export type IntakeMsg =
  | { tag: 'SetAnswer'; key: keyof Answers; value: string }
  | { tag: 'Next' }
  | { tag: 'Back' }
  | { tag: 'GaNaarStap'; cursor: number }
  | { tag: 'Submit' }
  | { tag: 'Retry' }
  | { tag: 'SubmitConfirmed' }
  | { tag: 'SubmitFailed'; error: string }
  | { tag: 'SetPolicy'; scholingThreshold: number }
  | { tag: 'Seed'; state: IntakeState };

export function reduce(s: IntakeState, m: IntakeMsg): IntakeState {
  switch (m.tag) {
    case 'SetAnswer':
      return setAnswer(s, m.key, m.value);
    case 'Next':
      return next(s);
    case 'Back':
      return back(s);
    case 'GaNaarStap':
      return gaNaarStap(s, m.cursor);
    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 'SetPolicy':
      return setPolicy(s, m.scholingThreshold);
    case 'Seed':
      return m.state;
    default:
      return assertNever(m);
  }
}
