"use client";

import { useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
  api,
  CommissioningBoard,
  CommissioningVehicle,
  DeviceProfile,
  SelfTest,
  VehicleModel,
  UnclaimedDevice,
} from "@/lib/api";
import { useLocale } from "@/i18n/LocaleProvider";
import { useVehicleTypes } from "@/lib/useVehicleTypes";
import {
  Button,
  Card,
  Field,
  Numeric,
  PageHeader,
  SectionTitle,
  Select,
  Spinner,
  StatusBadge,
  Stepper,
  Tone,
} from "@/components/ui";
import { Icon } from "@/components/icons";
import { SelfTestControl } from "../SelfTestControl";

/**
 * Single-vehicle wizard - the exception path.
 *
 * <p>Bulk import is the normal way a fleet is onboarded; this exists for the vehicles that came
 * back red on the board, for a one-off replacement, and for the first device of a new batch when
 * nobody is yet sure the provisioning is right. It walks the whole job in order rather than
 * assuming the operator already knows which step they are on.
 */
const POLL_MS = 3000;

const MQTT_ENDPOINT =
  process.env.NEXT_PUBLIC_MQTT_ENDPOINT ?? "mqtt.beebbeeb.sa.com:1883";

type Step = 1 | 2 | 3 | 4 | 5;

/** All the later steps need of a freshly created vehicle. */
type NewVehicle = { id: string; qrCode: string };

export default function Page() {
  const { t } = useLocale();
  const router = useRouter();
  const [step, setStep] = useState<Step>(1);
  const [device, setDevice] = useState<UnclaimedDevice | null>(null);
  const [vehicle, setVehicle] = useState<NewVehicle | null>(null);
  const [heardBuzzer, setHeardBuzzer] = useState(false);
  const [selfTest, setSelfTest] = useState<SelfTest | null>(null);

  return (
    <div className="mx-auto max-w-3xl">
      <PageHeader
        titleKey="wiz.title"
        actions={
          <Link href="/fleet">
            <Button variant="ghost">{t("wiz.toBoard")}</Button>
          </Link>
        }
      />

      <div className="mb-6">
        <Stepper step={step} total={5} />
        <div className="mt-2 flex justify-between text-xs text-muted">
          <span>{t("wiz.step1")}</span>
          <span>{t("wiz.step2")}</span>
          <span>{t("wiz.step3")}</span>
          <span>{t("wiz.step4")}</span>
          <span>{t("wiz.step5")}</span>
        </div>
      </div>

      {step === 1 && <Prepare onNext={() => setStep(2)} />}

      {step === 2 && (
        <Detect
          selected={device}
          onSelect={setDevice}
          onBack={() => setStep(1)}
          onNext={() => setStep(3)}
        />
      )}

      {step === 3 && device && (
        <Bind
          device={device}
          onBack={() => setStep(2)}
          onCreated={(v) => {
            setVehicle(v);
            setStep(4);
          }}
        />
      )}

      {step === 4 && vehicle && (
        <Test
          vehicle={vehicle}
          heardBuzzer={heardBuzzer}
          onHeard={setHeardBuzzer}
          onSelfTest={setSelfTest}
          onBack={() => setStep(3)}
          onNext={() => setStep(5)}
        />
      )}

      {step === 5 && vehicle && (
        <Release
          vehicle={vehicle}
          selfTestPassed={selfTest?.state === "PASSED"}
          onDone={() => router.push("/fleet")}
          onAnother={() => {
            setDevice(null);
            setVehicle(null);
            setSelfTest(null);
            setHeardBuzzer(false);
            setStep(1);
          }}
        />
      )}
    </div>
  );
}

/* ------------------------------------------------------------------ step 1 */

function Prepare({ onNext }: { onNext: () => void }) {
  const { t } = useLocale();
  const [copied, setCopied] = useState<string | null>(null);
  // Both are per-unit facts we cannot know: the SIM's carrier decides the APN, and the device
  // password can be changed on the unit. Asking for them here is what makes Copy produce a string
  // that actually works - the old version pasted a literal "<your-apn>" for someone to hand-edit.
  const [password, setPassword] = useState("123456");
  const [apn, setApn] = useState("");

  const [host, port] = [
    MQTT_ENDPOINT.split(":")[0],
    MQTT_ENDPOINT.split(":")[1] ?? "1883",
  ];
  const ready = password.trim().length > 0 && apn.trim().length > 0;

  /** OMNI SMS configuration, in the order the device needs it. */
  const commands = [
    { labelKey: "wiz.cmdIp", value: `pw,${password.trim()},ip,${host},${port}#` },
    { labelKey: "wiz.cmdApn", value: `pw,${password.trim()},apn,${apn.trim()}#` },
    { labelKey: "wiz.cmdServer", value: `pw,${password.trim()},server,1#` },
  ];

  function copy(value: string) {
    navigator.clipboard?.writeText(value);
    setCopied(value);
    setTimeout(() => setCopied(null), 1500);
  }

  return (
    <Card className="space-y-5">
      <div>
        <SectionTitle>{t("wiz.prepareTitle")}</SectionTitle>
        <p className="text-sm text-muted">{t("wiz.prepareBody")}</p>
      </div>

      <div className="rounded-card border border-line bg-background p-4">
        <p className="text-xs font-medium text-muted">{t("wiz.broker")}</p>
        <p className="mt-1 font-mono text-sm" dir="ltr">
          {MQTT_ENDPOINT}
        </p>
        <p className="mt-2 text-xs text-muted">{t("wiz.brokerHint")}</p>
      </div>

      <div className="grid gap-4 sm:grid-cols-2">
        <div>
          <Field
            label={t("wiz.devicePassword")}
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            dir="ltr"
          />
          <p className="mt-1.5 text-xs text-muted">{t("wiz.devicePasswordHint")}</p>
        </div>
        <div>
          <Field
            label={t("wiz.apn")}
            value={apn}
            onChange={(e) => setApn(e.target.value)}
            placeholder={t("wiz.apnPlaceholder")}
            dir="ltr"
          />
          <p className="mt-1.5 text-xs text-muted">{t("wiz.apnHint")}</p>
        </div>
      </div>

      <div>
        <p className="mb-2 text-sm text-muted">{t("wiz.smsHow")}</p>
        <ol className="space-y-2">
          {commands.map((c, i) => (
            <li
              key={c.labelKey}
              className="rounded-card border border-line bg-background p-3"
            >
              <p className="mb-1.5 text-xs font-medium text-muted">
                <Numeric>{i + 1}</Numeric>. {t(c.labelKey as never)}
              </p>
              <div className="flex items-center gap-3">
                <code
                  className={`min-w-0 flex-1 truncate font-mono text-xs ${
                    ready ? "" : "opacity-40"
                  }`}
                  dir="ltr"
                >
                  {c.value}
                </code>
                <Button
                  variant="ghost"
                  onClick={() => copy(c.value)}
                  disabled={!ready}
                >
                  {copied === c.value ? t("wiz.copied") : t("wiz.copy")}
                </Button>
              </div>
            </li>
          ))}
        </ol>
        {!ready && <p className="mt-2 text-xs text-warning">{t("wiz.fillFirst")}</p>}
      </div>

      <p className="text-xs text-muted">{t("wiz.prepareNote")}</p>

      <div className="flex justify-end">
        <Button variant="primary" onClick={onNext}>
          {t("wiz.next")}
        </Button>
      </div>
    </Card>
  );
}

/* ------------------------------------------------------------------ step 2 */

function Detect({
  selected,
  onSelect,
  onBack,
  onNext,
}: {
  selected: UnclaimedDevice | null;
  onSelect: (d: UnclaimedDevice) => void;
  onBack: () => void;
  onNext: () => void;
}) {
  const { t } = useLocale();
  const [devices, setDevices] = useState<UnclaimedDevice[] | null>(null);

  // Polls rather than loads once: the whole step is "wait for hardware to appear".
  useEffect(() => {
    let live = true;
    const tick = async () => {
      try {
        const board: CommissioningBoard = await api.commissioningBoard();
        if (live) setDevices(board.unclaimed);
      } catch {
        if (live) setDevices([]);
      }
    };
    tick();
    const id = setInterval(tick, POLL_MS);
    return () => {
      live = false;
      clearInterval(id);
    };
  }, []);

  return (
    <Card className="space-y-4">
      <div>
        <SectionTitle>{t("wiz.detectTitle")}</SectionTitle>
        <p className="text-sm text-muted">{t("wiz.detectBody")}</p>
      </div>

      {devices === null && <Spinner />}

      {devices !== null && devices.length === 0 && (
        <div className="rounded-card border border-line bg-background p-6 text-center">
          <div className="mb-2 flex items-center justify-center gap-2 text-sm text-muted">
            <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-brand" />
            {t("wiz.detectWaiting")}
          </div>
          <p className="text-xs text-muted">{t("wiz.detectNone")}</p>
        </div>
      )}

      {devices !== null && devices.length > 0 && (
        <ul className="divide-y divide-line rounded-card border border-line">
          {devices.map((d) => {
            const isSelected = selected?.imei === d.imei;
            return (
              <li key={d.imei} className="flex items-center gap-4 p-4">
                <div className="min-w-0 flex-1">
                  <p className="truncate text-sm font-semibold">
                    <Numeric>{d.imei}</Numeric>
                  </p>
                  <p className="truncate text-xs text-muted">
                    {[d.deviceType, d.hardwareType].filter(Boolean).join(" · ") || "-"}
                    {d.batteryPercent != null && (
                      <>
                        {" · "}
                        <Numeric>{d.batteryPercent}%</Numeric>
                      </>
                    )}
                  </p>
                </div>
                <Button
                  variant={isSelected ? "primary" : "secondary"}
                  onClick={() => onSelect(d)}
                >
                  {isSelected ? t("wiz.selected") : t("wiz.select")}
                </Button>
              </li>
            );
          })}
        </ul>
      )}

      <div className="flex justify-between">
        <Button onClick={onBack}>{t("wiz.back")}</Button>
        <Button variant="primary" onClick={onNext} disabled={!selected}>
          {t("wiz.next")}
        </Button>
      </div>
    </Card>
  );
}

/* ------------------------------------------------------------------ step 3 */

function Bind({
  device,
  onBack,
  onCreated,
}: {
  device: UnclaimedDevice;
  onBack: () => void;
  onCreated: (v: NewVehicle) => void;
}) {
  const { t } = useLocale();
  const [qrCode, setQrCode] = useState("");
  const [model, setModel] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [profiles, setProfiles] = useState<DeviceProfile[]>([]);
  const [models, setModels] = useState<VehicleModel[]>([]);
  const [modelId, setModelId] = useState("");
  // Pre-selected from what the device was actually heard speaking during detection, which is the
  // right answer far more often than any default, and makes the protocol check pass by default.
  const [profile, setProfile] = useState(device.profileId ?? "");
  const vehicleTypes = useVehicleTypes();
  // Defaulted from what the hardware said it is, then overridable. The mapping lives on the
  // catalogue row (protocolName), so a HorseLock is recognised too and an operator-defined type
  // simply has no hardware name to match, which is correct.
  const detectedType =
    vehicleTypes.types.find(
      (x) => x.protocolName && x.protocolName.toLowerCase() === (device.deviceType ?? "").toLowerCase(),
    )?.code ?? "SCOOTER";
  const [vehicleType, setVehicleType] = useState("");
  const chosenType = vehicleType || detectedType;

  useEffect(() => {
    api.deviceProfiles().then(setProfiles).catch(() => setProfiles([]));
    api.vehicleModels().then(setModels).catch(() => setModels([]));
  }, []);

  const selectedProfile = profiles.find((p) => p.id === profile) ?? null;

  /**
   * Creates the vehicle through the import path, not plain registration.
   *
   * <p>That is deliberate: `registerVehicle` leaves a vehicle OFFLINE, which would promote itself
   * to AVAILABLE on its first heartbeat - the exact "untested vehicle on the rider map" problem
   * commissioning exists to prevent. Import is the path that lands a vehicle in COMMISSIONING, so
   * a wizard-created vehicle is gated identically to a bulk-imported one.
   */
  async function create() {
    setBusy(true);
    setError(null);
    try {
      const result = await api.importVehicles(false, [
        {
          qrCode: qrCode.trim(),
          imei: device.imei,
          model: model.trim() || undefined,
          hardwareType: device.hardwareType ?? undefined,
          vehicleType: chosenType,
          iotProfile: profile || undefined,
        },
      ]);
      if (!result.applied) {
        setError(result.errors[0]?.message ?? t("common.error"));
        return;
      }
      // The board is where the next step reads its checks from, so resolve the new id there.
      const board = await api.commissioningBoard();
      const created = board.vehicles.find((v) => v.qrCode === qrCode.trim());
      if (!created) {
        setError(t("common.error"));
        return;
      }
      onCreated({ id: created.id, qrCode: created.qrCode });
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e));
    } finally {
      setBusy(false);
    }
  }

  return (
    <Card className="space-y-4">
      <div>
        <SectionTitle>{t("wiz.bindTitle")}</SectionTitle>
        <p className="text-sm text-muted">{t("wiz.bindBody")}</p>
      </div>

      <div className="rounded-card border border-line bg-background p-3 text-sm">
        <span className="text-muted">IMEI </span>
        <Numeric>{device.imei}</Numeric>
        {device.hardwareType && (
          <span className="text-muted"> · {device.hardwareType}</span>
        )}
      </div>

      <Field
        label={t("wiz.qr")}
        value={qrCode}
        onChange={(e) => setQrCode(e.target.value)}
        placeholder="BEEB-0001"
        dir="ltr"
      />
      {/* Prefilled from the device's own report, but an operator running a kind the hardware
          cannot name has to be able to say so. */}
      <Select
        label={t("fleet.colType")}
        value={chosenType}
        onChange={(e) => setVehicleType(e.target.value)}
        options={vehicleTypes.options}
      />

      {/* A choice, not a text box: the import path links a model by name, and a typed variant
          spelling would silently create a second model nobody can group by. */}
      <Select
        label={t("wiz.model")}
        value={modelId}
        onChange={(e) => {
          const next = e.target.value;
          setModelId(next);
          const picked = models.find((m) => m.id === next);
          setModel(picked?.name ?? "");
          // The model's usual protocol is a good default, but never overrides what the device was
          // actually heard speaking during detection.
          if (!profile && picked?.defaultIotProfile) {
            setProfile(picked.defaultIotProfile);
          }
        }}
        options={models
          .filter((m) => m.active)
          .map((m) => ({ value: m.id, label: m.name }))}
        placeholder={t("fleet.modelNotSet")}
      />

      <div>
        <Select
          label={t("wiz.profile")}
          value={profile}
          onChange={(e) => setProfile(e.target.value)}
          placeholder={t("wiz.profileDefault")}
          // Grouped so the difference between "works now" and "recorded only" is visible in the
          // list itself, not buried in a note somebody scrolls past.
          options={[...profiles]
            .sort((a, b) => (a.status === b.status ? 0 : a.status === "SUPPORTED" ? -1 : 1))
            .map((p) => ({
              value: p.id,
              label: `${p.vendor}${p.model ? " " + p.model : ""} (${p.transport}) - ${
                p.status === "SUPPORTED"
                  ? t("wiz.profileSupported")
                  : t("wiz.profilePlanned")
              }`,
            }))}
        />
        <p className="mt-1.5 text-xs text-muted">{t("wiz.profileHint")}</p>
        {selectedProfile?.status === "PLANNED" && (
          <div className="card tint tone-warning mt-2 p-3">
            <p className="text-xs">{t("wiz.profilePlannedWarning")}</p>
          </div>
        )}
      </div>

      {error && <p className="text-sm text-danger">{error}</p>}

      <div className="flex justify-between">
        <Button onClick={onBack}>{t("wiz.back")}</Button>
        <Button
          variant="primary"
          onClick={create}
          disabled={busy || qrCode.trim().length === 0}
        >
          {t("wiz.create")}
        </Button>
      </div>
    </Card>
  );
}

/* ------------------------------------------------------------------ step 4 */

function Test({
  vehicle,
  heardBuzzer,
  onHeard,
  onSelfTest,
  onBack,
  onNext,
}: {
  vehicle: NewVehicle;
  heardBuzzer: boolean;
  onHeard: (v: boolean) => void;
  /** Lifted so step 5 can gate release on the lock test having actually passed. */
  onSelfTest: (r: SelfTest) => void;
  onBack: () => void;
  onNext: () => void;
}) {
  const { t } = useLocale();
  const [checks, setChecks] = useState<CommissioningVehicle | null>(null);

  useEffect(() => {
    let live = true;
    const tick = async () => {
      try {
        const board = await api.commissioningBoard();
        const mine = board.vehicles.find((v) => v.id === vehicle.id) ?? null;
        if (live) setChecks(mine);
      } catch {
        /* keep the last good render; the next tick retries */
      }
    };
    tick();
    const id = setInterval(tick, POLL_MS);
    return () => {
      live = false;
      clearInterval(id);
    };
  }, [vehicle.id]);

  const checkTone: Record<string, Tone> = {
    PASS: "success",
    FAIL: "danger",
    WAITING: "neutral",
  };

  const labels: Record<string, string> = {
    heartbeat: "com.checkHeartbeat",
    gps: "com.checkGps",
    battery: "com.checkBattery",
    signal: "com.checkSignal",
    protocol: "com.checkProtocol",
  };

  return (
    <Card className="space-y-5">
      <div>
        <SectionTitle>{t("wiz.testTitle")}</SectionTitle>
        <p className="text-sm text-muted">{t("wiz.testBody")}</p>
      </div>

      <div className="flex flex-wrap gap-2">
        {(checks?.checks ?? []).map((c) => (
          <span key={c.name}>
            <StatusBadge tone={checkTone[c.state]}>
              {t(labels[c.name] as never)}
              <span className="ms-1 opacity-70">
                {c.value && <Numeric>{c.value}</Numeric>}
                {c.value && c.code ? " " : ""}
                {c.code ? t(`check.${c.code}` as never) : ""}
              </span>
            </StatusBadge>
          </span>
        ))}
        {!checks && <Spinner />}
      </div>

      <div className="border-t border-line pt-4">
        <SelfTestControl vehicleId={vehicle.id} onFinished={onSelfTest} />
      </div>

      <div className="flex items-center gap-3 border-t border-line pt-4">
        <Button
          variant={heardBuzzer ? "primary" : "secondary"}
          onClick={() => onHeard(!heardBuzzer)}
        >
          {heardBuzzer ? t("wiz.heardIt") : t("wiz.notHeard")}
        </Button>
        <p className="text-xs text-muted">{t("com.ring")}</p>
      </div>

      <div className="flex justify-between">
        <Button onClick={onBack}>{t("wiz.back")}</Button>
        <Button variant="primary" onClick={onNext}>
          {t("wiz.next")}
        </Button>
      </div>
    </Card>
  );
}

/* ------------------------------------------------------------------ step 5 */

function Release({
  vehicle,
  selfTestPassed,
  onDone,
  onAnother,
}: {
  vehicle: NewVehicle;
  selfTestPassed: boolean;
  onDone: () => void;
  onAnother: () => void;
}) {
  const { t } = useLocale();
  const [released, setReleased] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [ready, setReady] = useState(false);

  useEffect(() => {
    api
      .commissioningBoard()
      .then((b) => setReady(b.vehicles.find((v) => v.id === vehicle.id)?.ready ?? false))
      .catch(() => setReady(false));
  }, [vehicle.id]);

  const canRelease = ready && selfTestPassed;

  async function release() {
    setBusy(true);
    setError(null);
    try {
      await api.commissionVehicle(vehicle.id);
      setReleased(true);
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e));
    } finally {
      setBusy(false);
    }
  }

  if (released) {
    return (
      <Card className="flex flex-col items-center gap-4 p-10 text-center">
        <span className="text-success">
          <Icon name="commissioning" />
        </span>
        <p className="text-lg font-semibold">{t("wiz.done")}</p>
        <p className="text-sm text-muted">{vehicle.qrCode}</p>
        <div className="flex gap-3">
          <Button onClick={onAnother}>{t("wiz.addAnother")}</Button>
          <Button variant="primary" onClick={onDone}>
            {t("wiz.toBoard")}
          </Button>
        </div>
      </Card>
    );
  }

  return (
    <Card className="space-y-4">
      <div>
        <SectionTitle>{t("wiz.releaseTitle")}</SectionTitle>
        <p className="text-sm text-muted">
          {canRelease ? t("wiz.releaseBody") : t("wiz.releaseBlocked")}
        </p>
      </div>

      {error && <p className="text-sm text-danger">{error}</p>}

      <div className="flex justify-end">
        <Button
          variant="primary"
          onClick={release}
          disabled={busy || !canRelease}
        >
          {t("com.commission")}
        </Button>
      </div>
    </Card>
  );
}
