/**
 * Port des utilitaires workflow PWA (public/js/workflow-utils.js).
 *
 * L'app mobile applique la MEME logique que la PWA : groupement par phase,
 * detection des valeurs "probleme/panne" pour deploiement des extensions,
 * filtrage des steps decoratifs. Garder ces helpers ici permet d'ecrire
 * des tests unitaires et de rester aligne avec la logique web.
 */

import * as Crypto from "expo-crypto";
import type { MobileStep, StepValue } from "../types/intervention";

type StepMetadata = {
  phase?: string | null;
  is_extension?: boolean;
  input_kind?: string;
  requires_comment?: boolean;
  comment_rule?: string | null;
  comment_text?: string | null;
  action_label?: string | null;
  note?: string | null;
  // Override affichage-info phase 1 (CERFA / prelevement huile / test acide)
  // calcule par le serveur (applyPhase1CerfaInfoOverride). display_only=true ->
  // box d'info en lecture seule, auto_message = le texte a afficher.
  display_only?: boolean;
  auto_message?: string | null;
  auto_message_emphasis?: string | null;
  // Texte d'affichage resolu cote serveur (option backend du handoff #155
  // pour les etapes "LISTE DEVIS *"). Quand present, le mobile l'affiche tel
  // quel sans avoir a charger /api/devis.php cote client.
  display_content?: string | null;
};

export function getStepMetadata(step: MobileStep): StepMetadata {
  return (step.metadata || {}) as StepMetadata;
}

/**
 * Genere un UUID v4 — utilise pour idempotency_key des actions workflow.
 * La colonne workflow_idempotency_keys.idempotency_key est CHAR(36) strict :
 * tout key qui depasse 36 chars est tronque par MySQL et casse l'unicite.
 * Mirror du uuidv4 de public/js/workflow-utils.js.
 */
export function uuidv4(): string {
  // crypto.randomUUID dispo en RN 0.74+ via standards Web. Fallback sur
  // expo-crypto.randomUUID qui s'appuie sur le CSPRNG de l'OS : pas de
  // Math.random (insecure RNG flaggue par Sonar S2245).
  const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;
  if (c && typeof c.randomUUID === "function") {
    return c.randomUUID();
  }
  return Crypto.randomUUID();
}

/**
 * Normalise pour comparaison insensible a la casse / accents — identique
 * a la PWA pour que la detection de probleme soit coherente.
 */
function safeToString(v: unknown): string {
  if (v === null || v === undefined) return "";
  if (typeof v === "string") return v;
  if (typeof v === "number" || typeof v === "boolean") return String(v);
  return JSON.stringify(v);
}

export function normalizeText(v: unknown): string {
  return safeToString(v)
    .normalize("NFD")
    .replaceAll(/[\u0300-\u036f]/g, "")
    .trim()
    .toLowerCase();
}

/**
 * Une valeur denote un "probleme / panne" si elle contient un des mots-cles
 * qui declenchent le deploiement des sous-etapes (extensions).
 */
export function isProblemValue(v: unknown): boolean {
  const n = normalizeText(v);
  if (n === "") {
    return false;
  }
  const keywords = ["probleme", "panne", "ko", "defaut", "non conforme", "a revoir"];
  return keywords.some((x) => n.includes(x));
}

/**
 * Portage de public/js/workflow-utils.js evaluateVisibilityRule (refonte
 * coolcare 2026-04-24). Evalue les primitives de visibilite explicites
 * (rules_json serveur, recues dans step.constraints cote mobile) d'une
 * extension par rapport a la valeur de son parent positionnel.
 *
 * Retour :
 *   true  : extension visible
 *   false : extension cachee
 *   null  : aucune primitive _parent applicable -> l'appelant retombe sur le
 *           fallback positionnel isProblemValue(parentValue) (retro-compat
 *           templates pre-refonte type BLOC HYDRAULIQUE).
 *
 * Comparaison stricte sur la string brute (pas de normalisation casse) : les
 * options stockees en BDD sont la source de verite des valeurs.
 */
export function evaluateVisibilityRule(
  rules: Record<string, unknown> | null | undefined,
  parentValue: unknown
): boolean | null {
  if (!rules || typeof rules !== "object") {
    return null;
  }

  // Parent pas encore renseigne -> extension cachee par defaut. Sans ce
  // garde-fou, visible_if_not_parent renvoie true quand parentValue est vide.
  const pvRaw = parentValue === null || parentValue === undefined ? "" : String(parentValue);
  const pvEmpty = pvRaw === "" || pvRaw === "-";

  if (Object.hasOwn(rules, "visible_if_not_parent")) {
    if (pvEmpty) {
      return false;
    }
    return pvRaw !== String((rules as { visible_if_not_parent?: unknown }).visible_if_not_parent ?? "");
  }
  if (Object.hasOwn(rules, "visible_if_in_parent")) {
    const allowed = (rules as { visible_if_in_parent?: unknown }).visible_if_in_parent;
    if (!Array.isArray(allowed) || pvEmpty) {
      return false;
    }
    return allowed.some((v) => String(v ?? "") === pvRaw);
  }
  return null;
}

/**
 * Les rules de visibilite explicites arrivent du backend dans step.constraints
 * (mappe depuis rules_json par mobileStepFromRow). On les expose telles quelles
 * a evaluateVisibilityRule.
 */
function getStepRules(step: MobileStep): Record<string, unknown> | null {
  const c = step.constraints;
  return c && typeof c === "object" ? (c as Record<string, unknown>) : null;
}

/**
 * Gate step : boolean/choice dont le label contient "commencer".
 * Utilise pour demarrer l'intervention, pas une vraie etape metier.
 */
export function isGateStep(step: MobileStep): boolean {
  const meta = getStepMetadata(step);
  const kind = String(meta.input_kind || "");
  if (kind !== "boolean" && kind !== "choice") {
    return false;
  }
  return normalizeText(step.label).includes("commencer");
}

export type WorkflowPhase = {
  name: string;
  steps: MobileStep[];
};

/**
 * Regroupe les etapes par phase (champ metadata.phase), en conservant
 * l'ordre de order_index. Les etapes gate (boolean "Commencer ?") sont
 * filtrees car gerees par un bouton dedie dans l'UX.
 */
export function buildPhases(steps: MobileStep[]): WorkflowPhase[] {
  const sorted = [...steps]
    .filter((s) => !isGateStep(s))
    .sort((a, b) => a.order_index - b.order_index);

  const phases: WorkflowPhase[] = [];
  let lastPhaseName: string | null = null;
  let currentPhase: WorkflowPhase | null = null;

  for (const step of sorted) {
    const meta = getStepMetadata(step);
    const rawName = (meta.phase || "").toString().trim();
    const phaseName: string = rawName || lastPhaseName || "—";
    if (phaseName !== lastPhaseName || !currentPhase) {
      currentPhase = { name: phaseName, steps: [] };
      phases.push(currentPhase);
      lastPhaseName = phaseName;
    }
    currentPhase.steps.push(step);
  }

  return phases;
}

/**
 * Calcule la visibilite des extensions dans une phase en fonction des valeurs
 * en cours (draft) des etapes parent (choice/boolean).
 *
 * Miroir fidele de updateExtensions (public/intervention_etapes.php) :
 *   - On parcourt la phase dans l'ordre ; `lastParent` = derniere etape parent
 *     non-extension choice/boolean (non auto-calc) rencontree, avec sa valeur.
 *   - Une extension est visible si elle a un parent ET que :
 *       * une primitive explicite (visible_if_in_parent / visible_if_not_parent
 *         dans constraints) tranche la visibilite, SINON
 *       * fallback positionnel isProblemValue(valeur du parent).
 *   - Une extension choice/boolean ne devient parent des suivantes que si SA
 *     valeur est elle-meme un probleme (imbrication reelle) ; cachee, elle
 *     coupe la chaine en cascade.
 *
 * Retourne un Set des step.id visibles.
 */
type StepParent = { key: string; value: unknown };

// Visibilite d'une etape NON-extension : tranchee cote serveur (is_visible).
// Renvoie le parent a propager (choice/boolean -> nouveau parent, sinon coupe).
function resolveNonExtensionStep(
  step: MobileStep,
  isParentType: boolean,
  value: unknown,
  visible: Set<string>
): StepParent | null {
  // Sa visibilite est tranchee cote serveur (isStepVisible ->
  // resolveMachineTestStepVisibility pour huile/acide, sections, etc.) et ne
  // depend PAS d'une saisie offline. On respecte donc le flag is_visible
  // serveur. Sans ca, "NUMERO PRELEVEMENT HUILE" ET "Si acide test // Photo"
  // (mutuellement exclusives, l'une marquee is_visible=false) s'affichaient
  // toutes deux depuis que les endpoints renvoient aussi les etapes masquees
  // pour l'offline-first (handoff #168 / commit 64b2d7fe). La PWA, elle, filtre
  // bien sur is_visible. Seules les extensions sont recalculees ici.
  if (step.is_visible === false) {
    // Masquee serveur : ni visible, ni parent d'une chaine d'extensions.
    return null;
  }
  visible.add(step.id);
  // Parent choice/boolean -> nouveau parent ; sinon coupe la chaine.
  return isParentType ? { key: step.id, value } : null;
}

// Visibilite d'une etape EXTENSION : primitives explicites d'abord, fallback
// isProblemValue positionnel. Renvoie le parent a propager pour la suite.
function resolveExtensionStep(
  step: MobileStep,
  isParentType: boolean,
  lastParent: StepParent | null,
  value: unknown,
  visible: Set<string>
): StepParent | null {
  const ruleResult = evaluateVisibilityRule(getStepRules(step), lastParent?.value);
  const show = lastParent !== null && (ruleResult ?? isProblemValue(lastParent.value));

  if (!show) {
    // Extension cachee qui est elle-meme parent potentiel : coupe en cascade.
    return isParentType ? null : lastParent;
  }
  visible.add(step.id);
  // Ne devient parent des suivantes que si SA valeur est un probleme ; sinon
  // on preserve le parent original (extensions sœurs visibles).
  if (isParentType && isProblemValue(value)) {
    return { key: step.id, value };
  }
  return lastParent;
}

export function computeVisibleStepIds(
  phase: WorkflowPhase,
  draftValues: Record<string, StepValue>
): Set<string> {
  const visible = new Set<string>();
  let lastParent: StepParent | null = null;

  for (const step of phase.steps) {
    const meta = getStepMetadata(step);
    const kind = String(meta.input_kind || "");
    const isParentType = (kind === "choice" || kind === "boolean") && !isAutoCalc(step);
    const value = draftValues[step.id] ?? step.value;

    lastParent = meta.is_extension
      ? resolveExtensionStep(step, isParentType, lastParent, value, visible)
      : resolveNonExtensionStep(step, isParentType, value, visible);
  }

  return visible;
}

/**
 * Valide qu'une phase est prete a passer : toutes les etapes requises
 * visibles ont une valeur non vide. Retourne la liste des step.id qui
 * bloquent, ou [] si tout est bon.
 */
export function getBlockingStepIds(
  phase: WorkflowPhase,
  draftValues: Record<string, StepValue>,
  visibleIds: Set<string>
): string[] {
  const blocking: string[] = [];

  for (const step of phase.steps) {
    if (!visibleIds.has(step.id)) {
      continue;
    }
    if (!step.required) {
      continue;
    }
    // Auto-calc et display-only ne bloquent jamais : c'est le serveur qui
    // remplit leur valeur (ou pas). Un "Duree vie depassee" vide est
    // acceptable (= pas de revision requise), le tech ne peut rien y faire.
    if (isAutoCalc(step) || isDisplayOnly(step)) {
      continue;
    }
    const value = draftValues[step.id] ?? step.value;
    if (isEmptyStepValue(value)) {
      blocking.push(step.id);
    }
  }

  return blocking;
}

export function isEmptyStepValue(value: StepValue): boolean {
  if (value === null || value === undefined) {
    return true;
  }
  if (typeof value === "string" && value.trim() === "") {
    return true;
  }
  if (Array.isArray(value) && value.length === 0) {
    return true;
  }
  if (typeof value === "object" && "signed" in value && !value.signed) {
    return true;
  }
  return false;
}

/**
 * Portage de public/js/workflow-utils.js isAutoCalc : une etape dont le
 * label de phase ou action contient "calcul automatique", ou dont l'action
 * match les cles t_condensation / t_ligne_* / t_evaporation, est calculee
 * par le serveur — le mobile la rend en lecture seule.
 */
export function isAutoCalc(step: MobileStep): boolean {
  const meta = getStepMetadata(step);
  const phaseNorm = normalizeText(meta.phase || "");
  const actionNorm = normalizeText(meta.action_label || "");

  // Le nom de phase peut etre "Calcul automatique" (singulier, PWA) ou
  // "Calculs automatiques" (pluriel, templates prod). On match les deux
  // via le prefixe "calcul".
  if (
    phaseNorm.includes("calculs automatiques") ||
    phaseNorm.includes("calcul automatique") ||
    actionNorm.includes("calcul automatique")
  ) {
    return true;
  }

  // Fallback pour les steps legacy avec phase=NULL : on detecte via les
  // patterns de formule dans action_label. Mirror exact de
  // InterventionWorkflowService::isAutomaticCalculationStep.
  const al = String(meta.action_label || "").toLowerCase();
  if (
    al.includes("t_condensation") ||
    al.includes("t_ligne_aspiration") ||
    al.includes("t_ligne_liquide") ||
    al.includes("t_evaporation")
  ) {
    return true;
  }

  // Pattern des "Duree vie depassee" : action_label de forme
  // "Si nbre heure > 32 000 h , duree de vie depassee". Le serveur matche
  // via la combo "nbre heure" + ">". Sans ca ces steps etaient editables
  // cote mobile alors qu'elles doivent rester read-only.
  if (al.includes("nbre heure") && al.includes(">")) {
    return true;
  }

  return false;
}

/**
 * Portage isCerfaLike : les prerequis dont l'item mentionne CERFA ou
 * prelevement huile s'affichent avec un toggle "Oui preparer" + input
 * conditionnel (cf renderStepBlock case 'prerequisite').
 */
export function isCerfaLike(step: MobileStep): boolean {
  const item = normalizeText(step.label);
  return item.includes("cerfa") || (item.includes("prelevement") && item.includes("huile"));
}

/**
 * Steps purement informatifs, pas d'input demande au tech. Rendu comme
 * box d'info en lecture seule (cf renderStepBlock default / display).
 */
export function isDisplayOnly(step: MobileStep): boolean {
  const meta = getStepMetadata(step);
  // Flag serveur prioritaire : applyPhase1CerfaInfoOverride pose display_only
  // sur les prerequis CERFA / prelevement huile / test acide de phase 1 pour
  // qu'ils s'affichent en info (destinee a la phase 2/3) sans saisie editable.
  if (meta.display_only === true) {
    return true;
  }
  const kind = String(meta.input_kind || "");
  return kind === "display" || kind === "section" || kind === "info";
}

/**
 * Devis minimal consomme par le rendu des etapes "LISTE DEVIS *".
 * Structurellement compatible avec DevisItem (services/devisApi.ts) : on
 * garde le type local pour que ce util reste sans dependance vers la couche
 * services et testable isolement.
 */
type DevisLike = {
  id: number;
  numero_devis?: string | null;
  statut?: string | null;
};

const DEVIS_EN_ATTENTE_RE = /LISTE\s+DEVIS\s+EN\s+ATTENTE/i;
const DEVIS_DERNIER_RE = /LISTE\s+DERNIER\s+DEVIS/i;

/**
 * Resout le contenu textuel des etapes d'affichage "LISTE DEVIS EN ATTENTE" /
 * "LISTE DERNIER DEVIS" a partir des devis de la machine. Mirror exact de la
 * PWA (public/js/intervention_etapes.js, renderDisplayContent lignes 238-248).
 * Retourne null si le label ne matche aucun des deux patterns (l'appelant
 * retombe alors sur le rendu par defaut : auto_message / note / label).
 * Le separateur entre numero et statut est un tiret demi-cadratin U+2013.
 */
export function resolveDevisStepText(
  label: string,
  devis: readonly DevisLike[]
): string | null {
  const lbl = String(label || "");
  if (DEVIS_EN_ATTENTE_RE.test(lbl)) {
    const pending = devis.filter((d) => String(d.statut) === "en_attente");
    if (pending.length === 0) {
      return "Aucun devis en attente.";
    }
    return pending
      .map((d) => `#${d.id} – ${d.numero_devis || "Sans numéro"} (${d.statut})`)
      .join(" | ");
  }
  if (DEVIS_DERNIER_RE.test(lbl)) {
    if (devis.length === 0) {
      return "Aucun devis disponible.";
    }
    const last = devis[0];
    return `#${last.id} – ${last.numero_devis || "Sans numéro"} (${last.statut || "-"})`;
  }
  return null;
}

/**
 * Selectionne les devis a rendre en LIGNES TAPPABLES (devis viewer) pour une
 * etape "LISTE DEVIS *", en miroir EXACT du filtrage de resolveDevisStepText :
 *   - "LISTE DEVIS EN ATTENTE" -> les devis statut "en_attente" ;
 *   - "LISTE DERNIER DEVIS"    -> le devis le plus recent (data[0]) seul.
 * Retourne null si le label ne matche aucun pattern (l'etape n'est pas une
 * liste devis cliquable) ; un tableau VIDE si c'est bien une liste devis mais
 * sans devis a afficher (l'appelant retombe alors sur le texte "Aucun devis...").
 */
export function selectDevisForStep<T extends DevisLike>(
  label: string,
  devis: readonly T[]
): T[] | null {
  const lbl = String(label || "");
  if (DEVIS_EN_ATTENTE_RE.test(lbl)) {
    return devis.filter((d) => String(d.statut) === "en_attente");
  }
  if (DEVIS_DERNIER_RE.test(lbl)) {
    return devis.length > 0 ? [devis[0]] : [];
  }
  return null;
}

/**
 * Indique si une etape est une etape d'affichage "LISTE DEVIS *" (qui necessite
 * de charger les devis de la machine pour resoudre son contenu).
 */
export function isDevisListStep(step: MobileStep): boolean {
  const lbl = String(step.label || "");
  return DEVIS_EN_ATTENTE_RE.test(lbl) || DEVIS_DERNIER_RE.test(lbl);
}

/**
 * Determine si une etape doit afficher un textarea de commentaire visible,
 * et si ce commentaire est obligatoire. Mirror PWA renderStepBlock :
 *   - requires_comment=true -> commentaire toujours visible.
 *     Obligatoire si la valeur est un probleme/panne, sinon optionnel.
 *   - extension && choice -> commentaire visible UNIQUEMENT si valeur
 *     est un probleme/panne, et alors obligatoire.
 */
export function getCommentRequirement(
  step: MobileStep,
  currentValue: unknown
): { visible: boolean; required: boolean; placeholder: string } {
  const meta = getStepMetadata(step);
  const requiresComment = Boolean(meta.requires_comment);
  const isExtension = Boolean(meta.is_extension);
  const inputKind = String(meta.input_kind || "");
  const isProblem = isProblemValue(currentValue);

  if (requiresComment) {
    return {
      visible: true,
      required: isProblem,
      placeholder: isProblem
        ? "Decrire le probleme ou la panne (obligatoire)..."
        : "Commentaire (si Probleme/Panne)...",
    };
  }

  if (isExtension && inputKind === "choice" && isProblem) {
    return {
      visible: true,
      required: true,
      placeholder: "Decrire le probleme ou la panne (obligatoire)...",
    };
  }

  return { visible: false, required: false, placeholder: "" };
}
