"use client";

import { useState, useTransition, type FormEvent } from "react";
import { useRouter } from "next/navigation";
import { Dialog } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import type { PlanRow } from "@/lib/db/queries/plans";
import type { PlanFeatureCatalogEntry } from "@/lib/db/queries/plan-features-catalog";

// mysql2 décode les colonnes JSON en JS direct (array/object), mais on accepte
// aussi les strings au cas où la colonne serait stockée en TEXT ailleurs.
// Bug #68 : sans cette défense, un Array passait dans JSON.parse → throw →
// catch silencieux → modale vide alors que la BDD avait bien les données.
function parseFeaturesRaw(raw: unknown): string[] {
  if (raw == null) return [];
  let parsed: unknown = raw;
  if (typeof raw === "string") {
    try {
      parsed = JSON.parse(raw);
    } catch {
      return [];
    }
  }
  if (Array.isArray(parsed)) {
    return parsed.filter((x): x is string => typeof x === "string");
  }
  if (parsed && typeof parsed === "object") {
    return Object.entries(parsed as Record<string, unknown>)
      .filter(([, v]) => v === true || v === 1 || v === "1")
      .map(([k]) => k);
  }
  return [];
}

function parseBulletsRaw(raw: unknown): string[] {
  if (raw == null) return [];
  let parsed: unknown = raw;
  if (typeof raw === "string") {
    try {
      parsed = JSON.parse(raw);
    } catch {
      return [];
    }
  }
  if (Array.isArray(parsed)) {
    return parsed.filter((x): x is string => typeof x === "string");
  }
  return [];
}

const MAX_BULLETS = 20;
const MAX_BULLET_CHARS = 200;

// Découpe la textarea en bullets normalisés (trim + filter empty). Sert à la
// fois pour le compteur live et pour le payload PATCH.
function splitBullets(raw: string): string[] {
  return raw
    .split("\n")
    .map((s) => s.trim())
    .filter((s) => s.length > 0);
}

export function PlanEditModal({
  plan,
  featureCatalog,
  currentlyHighlighted,
  open,
  onClose,
}: {
  plan: PlanRow | null;
  featureCatalog: PlanFeatureCatalogEntry[];
  currentlyHighlighted: PlanRow | null;
  open: boolean;
  onClose: () => void;
}) {
  return (
    <Dialog
      open={open}
      onOpenChange={(o) => !o && onClose()}
      title={plan ? `Modifier le plan ${plan.name}` : "Modifier le plan"}
      description={plan?.code}
      size="xl"
    >
      {plan ? (
        <EditForm
          key={plan.id}
          plan={plan}
          featureCatalog={featureCatalog}
          currentlyHighlighted={currentlyHighlighted}
          onClose={onClose}
        />
      ) : null}
    </Dialog>
  );
}

function EditForm({
  plan,
  featureCatalog,
  currentlyHighlighted,
  onClose,
}: {
  plan: PlanRow;
  featureCatalog: PlanFeatureCatalogEntry[];
  currentlyHighlighted: PlanRow | null;
  onClose: () => void;
}) {
  const router = useRouter();
  const [pending, startTransition] = useTransition();
  const [error, setError] = useState<string | null>(null);

  const [name, setName] = useState(plan.name);
  const [description, setDescription] = useState(plan.description ?? "");
  const [monthly, setMonthly] = useState(String(plan.monthly_price));
  // yearly_price est calculé automatiquement = monthly × 10 (2 mois offerts).
  // Convention figée côté app (cf. mf_ask #77 + PlanService::getVisiblePlans).
  // Le panel n'envoie plus la valeur — le route handler la recalcule.
  // monthly est forcé entier côté UI (step=1) + serveur (coerceMoney) → pas
  // d'artefact float possible.
  const yearlyComputed = Number.isFinite(Number(monthly))
    ? Number(monthly) * 10
    : 0;
  // Quotas volumétriques (techniciens/sites/machines) : conservés en base mais
  // PLUS appliqués côté missioflow-app depuis le dé-gating per-seat (Lot 3,
  // mf_ask #133) — on facture le siège au lieu de bloquer. Au même titre que
  // max_admins, on les affiche en lecture seule et on renvoie la valeur
  // d'origine inchangée.
  const maxTech = plan.max_technicians;
  const maxSites = plan.max_sites;
  const maxMachines = plan.max_machines;
  const [maxTemplates, setMaxTemplates] = useState(plan.max_templates);
  // max_admins n'est plus éditable (plus appliqué côté app, mf_ask #103) — on
  // garde la valeur d'origine pour l'afficher et la renvoyer inchangée.
  const maxAdmins = plan.max_admins;
  const [visible, setVisible] = useState(plan.visible === 1);
  const [selectedFeatures, setSelectedFeatures] = useState<Set<string>>(
    () => new Set(parseFeaturesRaw(plan.features)),
  );

  // Marketing (page tarifs publique + vitrine Astro)
  const [ctaLabel, setCtaLabel] = useState(plan.cta_label ?? "");
  const [highlighted, setHighlighted] = useState(plan.highlighted === 1);
  const [bulletsRaw, setBulletsRaw] = useState(() =>
    parseBulletsRaw(plan.marketing_bullets).join("\n"),
  );
  const bullets = splitBullets(bulletsRaw);
  const tooManyBullets = bullets.length > MAX_BULLETS;
  const overflowingBullets = bullets
    .map((b, i) => ({ index: i + 1, len: b.length }))
    .filter((b) => b.len > MAX_BULLET_CHARS);
  const bulletsCountTone =
    bullets.length >= MAX_BULLETS - 1
      ? "text-danger"
      : bullets.length >= MAX_BULLETS - 5
        ? "text-warning"
        : "text-muted-foreground";

  function toggleFeature(code: string) {
    setSelectedFeatures((prev) => {
      const next = new Set(prev);
      if (next.has(code)) next.delete(code);
      else next.add(code);
      return next;
    });
  }

  async function onSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setError(null);

    // Validations marketing côté client (le serveur a le même slice/filter en
    // defense-in-depth, mais on évite de soumettre quand on sait que c'est KO).
    if (tooManyBullets) {
      setError(
        `Maximum ${MAX_BULLETS} bullets — retire-en ${bullets.length - MAX_BULLETS} avant d'enregistrer.`,
      );
      return;
    }
    if (overflowingBullets.length > 0) {
      const first = overflowingBullets[0];
      setError(
        `Bullet ligne ${first.index} : ${first.len} / ${MAX_BULLET_CHARS} chars — raccourcis-la.`,
      );
      return;
    }

    // Confirmation explicite si on déplace le badge "Le plus populaire" vers
    // un autre plan (cf. mf_answer #60 — la transaction côté serveur garantit
    // l'unicité, mais l'admin doit savoir ce qu'il déclenche).
    const isMovingHighlight =
      highlighted &&
      currentlyHighlighted !== null &&
      currentlyHighlighted.id !== plan.id;
    if (isMovingHighlight) {
      const ok = window.confirm(
        `${currentlyHighlighted.name} est actuellement mis en avant. Le remplacer par ${plan.name} ?`,
      );
      if (!ok) return;
    }

    const body = {
      name,
      description: description.trim().length > 0 ? description.trim() : null,
      monthly_price: Number(monthly),
      // yearly_price : pas envoyé — recalculé côté serveur depuis monthly_price
      max_technicians: maxTech,
      max_sites: maxSites,
      max_machines: maxMachines,
      max_templates: maxTemplates,
      max_admins: maxAdmins,
      visible,
      features: [...selectedFeatures],
      cta_label: ctaLabel.trim().length > 0 ? ctaLabel.trim() : null,
      highlighted,
      marketing_bullets: bullets,
    };
    try {
      const res = await fetch(`/api/superadmin/plans/${plan.id}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      const payload = (await res.json().catch(() => null)) as
        | { success: true }
        | {
            success: false;
            error?: string;
            message?: string;
            rejected?: string[];
          }
        | null;
      if (!res.ok || !payload || payload.success === false) {
        const msg =
          (payload && "rejected" in payload && payload.rejected?.length
            ? `Champs rejetés : ${payload.rejected.join(", ")}`
            : null) ||
          (payload && "message" in payload && payload.message) ||
          (payload && "error" in payload && payload.error) ||
          `HTTP ${res.status}`;
        setError(String(msg));
        return;
      }
      startTransition(() => {
        router.refresh();
        onClose();
      });
    } catch (err) {
      setError(err instanceof Error ? err.message : "Erreur réseau");
    }
  }

  // Le code est non-editable : un renommage casse les webhooks Stripe
  // checkout-completed (cf. mf_answer #9 + matrice mf_ask #10).
  const codeNote =
    plan.active_subscriptions > 0
      ? `Verrouillé — ${plan.active_subscriptions} abonnement(s) actif(s) y font référence`
      : "Verrouillé pour éviter de casser le webhook Stripe checkout-completed";

  return (
    <form onSubmit={onSubmit} className="space-y-4">
      <div className="grid gap-3 sm:grid-cols-2">
        <Field label="Nom (affiché publiquement)">
          <input
            type="text"
            required
            maxLength={100}
            value={name}
            onChange={(e) => setName(e.target.value)}
            className={inputCls}
          />
        </Field>
        <Field label="Code (lecture seule)">
          <input
            type="text"
            value={plan.code}
            disabled
            className={`${inputCls} cursor-not-allowed font-mono opacity-60`}
            title={codeNote}
          />
          <p className="mt-1 text-[10px] italic text-muted-foreground">
            🔒 {codeNote}
          </p>
        </Field>
        <Field label="Description (page tarifs + Stripe Products)">
          <textarea
            rows={2}
            maxLength={2000}
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            className={inputCls}
            placeholder="Description marketing — visible sur la page tarifs publique"
          />
        </Field>
        <Field label="Statut">
          <label className="mt-1 flex items-center gap-2 text-sm">
            <input
              type="checkbox"
              checked={visible}
              onChange={(e) => setVisible(e.target.checked)}
            />
            <span>Visible sur la page tarifs publique</span>
          </label>
        </Field>
      </div>

      {/* Bloc marketing : alimente à la fois la page tarifs publique de
          missioflow-app (signup, upgrade) et le site vitrine Astro
          (missioflow.fr). Source unique cf. mf_ask #57/#59. */}
      <fieldset className="rounded-lg border border-border bg-surface-2 p-3">
        <legend className="px-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
          Marketing (page tarifs publique + vitrine)
        </legend>
        <div className="grid gap-3 sm:grid-cols-2">
          <Field label="Libellé du bouton CTA">
            <input
              type="text"
              maxLength={64}
              value={ctaLabel}
              onChange={(e) => setCtaLabel(e.target.value)}
              className={inputCls}
              placeholder="Ex: Commencer, Choisir Pro"
            />
          </Field>
          <div className="sm:col-span-2">
            <Field
              label={`Bullets — 1 par ligne (max ${MAX_BULLETS}, ${MAX_BULLET_CHARS} chars chacun)`}
            >
              <textarea
                rows={6}
                value={bulletsRaw}
                onChange={(e) => setBulletsRaw(e.target.value)}
                className={inputCls}
                placeholder={
                  "Jusqu'à 3 techniciens\nMode offline (PWA)\nGénération PDF des rapports\n…"
                }
              />
              <div className="mt-1 flex items-center justify-between text-[10px]">
                <span className={`tabular-nums ${bulletsCountTone}`}>
                  {bullets.length} / {MAX_BULLETS} bullets
                </span>
                {overflowingBullets.length > 0 && (
                  <span className="text-danger">
                    {overflowingBullets.length} bullet
                    {overflowingBullets.length > 1 ? "s" : ""} trop longue
                    {overflowingBullets.length > 1 ? "s" : ""} (ligne{" "}
                    {overflowingBullets.map((b) => b.index).join(", ")})
                  </span>
                )}
              </div>
            </Field>
          </div>
          <div className="sm:col-span-2">
            <label className="flex items-center gap-2 text-sm">
              <input
                type="checkbox"
                checked={highlighted}
                onChange={(e) => setHighlighted(e.target.checked)}
              />
              <span>
                Mettre en avant ce plan (badge « Le plus populaire »)
                {currentlyHighlighted &&
                  currentlyHighlighted.id !== plan.id &&
                  highlighted && (
                    <span className="ml-2 text-[11px] text-warning">
                      ⚠ remplacera {currentlyHighlighted.name}
                    </span>
                  )}
              </span>
            </label>
          </div>
        </div>
      </fieldset>

      {/* Bloc prix avec warning Stripe */}
      <fieldset className="rounded-lg border border-amber-200 bg-warning-subtle p-3">
        <legend className="px-1 text-[11px] font-medium uppercase tracking-wider text-warning">
          Tarifs (⚠ désynchro Stripe possible)
        </legend>
        <p className="mb-3 text-[11px] leading-relaxed text-warning">
          Modifier le prix change <strong>l&apos;affichage de la page
          tarifs publique</strong> mais <strong>PAS les abonnements Stripe
          existants ni les nouveaux signups</strong> — le Stripe Price ID
          est immuable par design (conformité fiscale). Pour répercuter
          réellement, il faut re-tourner <code className="font-mono">bin/setup_stripe.php</code>{" "}
          côté missioflow-app puis migrer les subs existantes.
        </p>
        <div className="grid gap-3 sm:grid-cols-2">
          <Field label="Tarif mensuel (€)">
            <input
              type="number"
              min={0}
              step={1}
              required
              value={monthly}
              onChange={(e) => setMonthly(e.target.value)}
              className={inputCls}
            />
            <p className="mt-1 text-[10px] italic text-muted-foreground">
              Prix entier uniquement (€ ronds — pas de centimes).
            </p>
          </Field>
          <Field label="Tarif annuel (€) — calculé automatiquement = mensuel × 10">
            <input
              type="number"
              value={yearlyComputed}
              disabled
              className={`${inputCls} cursor-not-allowed opacity-60`}
              title="Calcul figé : 10 mois payés au lieu de 12 (2 mois offerts)"
            />
            <p className="mt-1 text-[10px] italic text-muted-foreground">
              Pour modifier le tarif annuel, ajustez le tarif mensuel.
            </p>
          </Field>
        </div>
      </fieldset>

      {/* Bloc quotas */}
      <fieldset className="rounded-lg border border-border bg-surface-2 p-3">
        <legend className="px-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
          Quotas du plan
        </legend>
        <p className="mb-3 text-[11px] italic text-muted-foreground">
          Astuce : entrez <code className="font-mono">-1</code> pour
          « illimité ». <code className="font-mono">0</code> = fonctionnalité
          désactivée pour ce plan.
        </p>
        <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
          <QuotaField
            label="Templates"
            value={maxTemplates}
            onChange={setMaxTemplates}
          />
        </div>
        {/* Quotas volumétriques conservés en base mais PLUS appliqués côté
            missioflow-app depuis le dé-gating per-seat (Lot 3, mf_ask #133 —
            facturation au siège plutôt que blocage) et le pricing volume-only
            pour les admins (2026-06-03, mf_ask #103). On les affiche en lecture
            seule pour ne pas laisser croire à un gating qui n'existe plus — les
            valeurs restent envoyées inchangées. */}
        <div className="mt-3 border-t border-border pt-3">
          <p className="mb-2 text-[11px] italic text-muted-foreground">
            ⓘ Quotas conservés en base mais non appliqués depuis le pricing
            per-seat (facturation au siège) — lecture seule.
          </p>
          <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
            <ReadOnlyQuota label="Techniciens (non appliqué)" value={maxTech} />
            <ReadOnlyQuota label="Sites (non appliqué)" value={maxSites} />
            <ReadOnlyQuota
              label="Machines (non appliqué)"
              value={maxMachines}
            />
            <ReadOnlyQuota
              label="Administrateurs (non appliqué)"
              value={maxAdmins}
            />
          </div>
        </div>
      </fieldset>

      {/* Bloc features = checklist depuis plan_features_catalog */}
      <fieldset className="rounded-lg border border-border bg-surface-2 p-3">
        <legend className="px-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
          Fonctionnalités incluses ({selectedFeatures.size} /{" "}
          {featureCatalog.length})
        </legend>
        {featureCatalog.length === 0 ? (
          <p className="text-xs italic text-muted-foreground">
            Catalogue de fonctionnalités introuvable — la table
            <code className="font-mono">plan_features_catalog</code> est
            peut-être vide.
          </p>
        ) : (
          <ul className="space-y-1.5">
            {featureCatalog.map((f) => {
              const checked = selectedFeatures.has(f.code);
              return (
                <li
                  key={f.code}
                  className={`flex gap-2 rounded-md border px-3 py-2 transition-colors ${
                    checked
                      ? "border-success bg-success-subtle"
                      : "border-border bg-surface"
                  }`}
                >
                  <label className="flex flex-1 cursor-pointer gap-2">
                    <input
                      type="checkbox"
                      checked={checked}
                      onChange={() => toggleFeature(f.code)}
                      className="mt-0.5 h-4 w-4 shrink-0"
                    />
                    <div className="min-w-0">
                      <div className="flex items-baseline gap-2 flex-wrap">
                        <span className="text-sm font-medium">{f.label}</span>
                        <code className="font-mono text-[10px] text-muted-foreground">
                          {f.code}
                        </code>
                      </div>
                      {f.description && (
                        <p className="mt-0.5 text-[11px] text-muted-foreground">
                          {f.description}
                        </p>
                      )}
                    </div>
                  </label>
                </li>
              );
            })}
          </ul>
        )}
      </fieldset>

      {error && (
        <div className="rounded-md border border-red-200 bg-danger-subtle px-3 py-2 text-sm text-danger">
          {error}
        </div>
      )}

      <div className="flex items-center justify-end gap-2 border-t border-border pt-3">
        <Button
          type="button"
          variant="ghost"
          onClick={onClose}
          disabled={pending}
        >
          Annuler
        </Button>
        <Button type="submit" loading={pending}>
          Enregistrer
        </Button>
      </div>
    </form>
  );
}

const inputCls =
  "mt-1 block w-full rounded-md border border-border-strong bg-surface px-3 py-2 text-sm text-foreground shadow-sm placeholder:text-subtle-foreground focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent-ring/40";

function Field({
  label,
  children,
}: {
  label: string;
  children: React.ReactNode;
}) {
  return (
    <div>
      <label className="block text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
        {label}
      </label>
      {children}
    </div>
  );
}

// Affichage lecture seule d'un quota conservé en base mais plus appliqué côté
// app (dé-gating per-seat). Rend "∞" pour -1, la valeur brute sinon.
function ReadOnlyQuota({ label, value }: { label: string; value: number }) {
  return (
    <Field label={label}>
      <input
        type="text"
        value={value === -1 ? "∞" : String(value)}
        disabled
        className={`${inputCls} cursor-not-allowed opacity-60`}
        title="Quota conservé en base mais plus appliqué (pricing per-seat)"
      />
    </Field>
  );
}

// Champ quota avec toggle ∞ rapide pour basculer "illimité" (= -1).
// Hint visuel quand la valeur est -1 ; sinon comportement standard d'un
// input number.
function QuotaField({
  label,
  value,
  onChange,
}: {
  label: string;
  value: number;
  onChange: (n: number) => void;
}) {
  const unlimited = value === -1;
  return (
    <Field label={label}>
      <div className="mt-1 flex gap-1">
        <input
          type="number"
          min={-1}
          required
          value={value}
          onChange={(e) => onChange(Number(e.target.value))}
          className={`${inputCls} mt-0 flex-1`}
        />
        <button
          type="button"
          onClick={() => onChange(unlimited ? 0 : -1)}
          title={unlimited ? "Limiter ce quota" : "Mettre illimité"}
          className={`shrink-0 rounded-md border px-2 text-sm font-bold leading-none transition-colors ${
            unlimited
              ? "border-success bg-success-subtle text-success"
              : "border-border bg-surface-2 text-muted-foreground hover:bg-surface-3"
          }`}
        >
          ∞
        </button>
      </div>
      {unlimited && (
        <p className="mt-1 text-[10px] font-medium text-success">
          Illimité (-1)
        </p>
      )}
    </Field>
  );
}
