import { useEffect, useMemo, useState } from "react";
import { Loader2, ShieldCheck } from "lucide-react";
import {
  Alert,
  Badge,
  Button,
  Group,
  Modal,
  NumberInput,
  Radio,
  Select,
  MultiSelect,
  SimpleGrid,
  Stack,
  Text,
  Textarea,
  TextInput,
} from "@mantine/core";

import {
  FLEET_SELECT_NONE,
  type FleetFormFieldDef,
} from "@/pages/fleet/config/resources";
import type { FleetRecord } from "@/services/fleet/fleet.service";
import {
  verifaydaService,
  type FaydaCallbackMessage,
} from "@/services/verifayda.service";

export interface FleetFormDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title: string;
  fields: FleetFormFieldDef[];
  initialRecord?: FleetRecord | null;
  emptyValues: Record<string, unknown>;
  isSubmitting: boolean;
  selectOptionsLoading?: boolean;
  onSubmit: (values: Record<string, unknown>) => void;
  /**
   * Show a "Verify with Fayda" step: opens the eSignet popup and prefills
   * firstName/lastName/email/phoneNumber/dateOfBirth from the verified
   * identity, stamping faydaVerified + faydaSub on the payload.
   */
  verifyWithFayda?: boolean;
}

// Fayda returns gender as "Male"/"Female"; snap it onto the form's uppercase
// option values (MALE/FEMALE/OTHER) so the Select prefills instead of rendering
// blank. Unknown/empty values fall through to undefined (field left untouched).
const normalizeGender = (raw?: string): string | undefined => {
  const up = (raw ?? "").trim().toUpperCase();
  if (up === "MALE" || up === "M") return "MALE";
  if (up === "FEMALE" || up === "F") return "FEMALE";
  return up ? "OTHER" : undefined;
};

// Fayda may return the birthdate as "2001/12/01" (slashes), but the date input
// and validator expect ISO "2001-12-01". Normalize separators + trim to 10 chars
// so the DOB field prefills instead of silently staying blank.
const normalizeBirthdate = (raw?: string): string | undefined => {
  const iso = (raw ?? "").trim().replace(/\//g, "-").slice(0, 10);
  return /^\d{4}-\d{2}-\d{2}$/.test(iso) ? iso : undefined;
};

const buildInitialValues = (
  fields: FleetFormFieldDef[],
  emptyValues: Record<string, unknown>,
  record?: FleetRecord | null,
): Record<string, unknown> => {
  const values: Record<string, unknown> = { ...emptyValues };
  if (!record) return values;

  fields.forEach((field) => {
    const raw = (record as unknown as Record<string, unknown>)[field.name];
    if (raw === null || raw === undefined) {
      values[field.name] = field.noneOption ? FLEET_SELECT_NONE : "";
      return;
    }
    // For selects, snap the record value onto a real option even if its casing
    // drifted (e.g. an API/seed value of "Available" vs the "AVAILABLE" option).
    // Otherwise the Select renders blank and a required field fails on submit.
    if (field.type === "select" && field.options?.length) {
      const match = field.options.find(
        (o) => String(o.value).toLowerCase() === String(raw).toLowerCase(),
      );
      values[field.name] = match ? match.value : raw;
      return;
    }
    values[field.name] = raw;
  });
  return values;
};

const FleetFormDialog = ({
  open,
  onOpenChange,
  title,
  fields,
  initialRecord,
  emptyValues,
  isSubmitting,
  selectOptionsLoading,
  onSubmit,
  verifyWithFayda,
}: FleetFormDialogProps) => {
  const [values, setValues] = useState<Record<string, unknown>>({});
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [faydaLoading, setFaydaLoading] = useState(false);
  const [faydaError, setFaydaError] = useState<string | null>(null);

  // Seed the form ONLY when the dialog opens or the edited record changes — NOT
  // when `fields`/`emptyValues` get new object refs (they're rebuilt whenever the
  // dynamic select options finish loading). Re-seeding on those would wipe the
  // user's in-progress edits (e.g. a changed Current Yard / status) the moment
  // the yard or wagon-type options resolve.
  const recordId =
    initialRecord && "id" in initialRecord ? String(initialRecord.id) : null;
  useEffect(() => {
    if (open) {
      setValues(buildInitialValues(fields, emptyValues, initialRecord));
      setErrors({});
      setFaydaError(null);
      setFaydaLoading(false);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [open, recordId]);

  // Seed the `_`-prefixed scratch that `onOptionSelected` derives (e.g.
  // _hasTrailer) for the value already on the record. Without this, editing a
  // rigid truck would show a Trailer Plate field until the type is re-picked.
  // Only scratch keys are written, so a stored one-off capacity is never
  // clobbered by the type's default; re-deriving from the live value is
  // idempotent, so this is safe to run again when the options finally load.
  useEffect(() => {
    if (!open) return;
    setValues((current) => {
      const scratch: Record<string, unknown> = {};
      fields.forEach((field) => {
        if (!field.onOptionSelected) return;
        const selected = field.options?.find((o) => o.value === current[field.name]);
        if (!selected) return;
        Object.entries(field.onOptionSelected(selected, current)).forEach(([key, value]) => {
          if (key.startsWith("_")) scratch[key] = value;
        });
      });
      return Object.keys(scratch).length ? { ...current, ...scratch } : current;
    });
  }, [open, fields]);

  // Receive the ?code&state relayed by the /callback popup, exchange it for
  // the verified identity, and prefill the matching form fields.
  useEffect(() => {
    if (!open || !verifyWithFayda) return;
    const onMessage = async (event: MessageEvent<FaydaCallbackMessage>) => {
      if (event.origin !== window.location.origin) return;
      if (event.data?.type !== "fayda-callback") return;

      if (event.data.error) {
        setFaydaLoading(false);
        setFaydaError(event.data.errorDescription ?? event.data.error);
        return;
      }
      if (!event.data.code || !event.data.state) return;

      try {
        const result = await verifaydaService.complete(event.data.code, event.data.state);
        if (!result.verified) {
          setFaydaError("Identity could not be verified");
          return;
        }
        const nameParts = (result.fullName ?? "").trim().split(/\s+/).filter(Boolean);
        const [firstName, ...rest] = nameParts;
        const gender = normalizeGender(result.gender);
        const dateOfBirth = normalizeBirthdate(result.birthdate);
        setValues((current) => ({
          ...current,
          ...(firstName ? { firstName } : {}),
          ...(rest.length ? { lastName: rest.join(" ") } : {}),
          ...(result.email ? { email: result.email } : {}),
          ...(result.phoneNumber ? { phoneNumber: result.phoneNumber } : {}),
          ...(dateOfBirth ? { dateOfBirth } : {}),
          ...(gender ? { gender } : {}),
          faydaVerified: true,
          ...(result.iamUserId ? { faydaSub: result.iamUserId } : {}),
        }));
        setFaydaError(null);
      } catch (err) {
        const message =
          (err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
          (err instanceof Error ? err.message : "Verification failed");
        setFaydaError(message);
      } finally {
        setFaydaLoading(false);
      }
    };
    window.addEventListener("message", onMessage);
    return () => window.removeEventListener("message", onMessage);
  }, [open, verifyWithFayda]);

  const handleFaydaVerify = async () => {
    setFaydaError(null);
    setFaydaLoading(true);
    try {
      const { authorizationUrl } = await verifaydaService.start();
      const popup = window.open(
        authorizationUrl,
        "fayda-verify",
        "width=480,height=760,noopener=no",
      );
      if (!popup) {
        setFaydaLoading(false);
        setFaydaError("Pop-up blocked — allow pop-ups for this site and retry.");
      }
      // Loading stays on until the popup posts back; reopening the dialog resets it.
    } catch (err) {
      setFaydaLoading(false);
      const message =
        (err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
        (err instanceof Error ? err.message : "Could not start verification");
      setFaydaError(message);
    }
  };

  const faydaVerified = values.faydaVerified === true;

  /**
   * Fields the current answers actually apply to — a rigid truck type (Casoni)
   * has no trailer, so its plate field disappears. Honoured in three places, not
   * just here: a hidden field must also skip validation (an invisible "required"
   * error blocks submit with nothing to fix) and must submit an explicit null
   * (so switching to a rigid type CLEARS the stored trailer plate rather than
   * stranding it on the row).
   */
  const visibleFields = useMemo(
    () =>
      fields.filter((field) => {
        if (
          field.hideWhen &&
          field.hideWhen.equals.includes(String(values[field.hideWhen.field] ?? ""))
        ) {
          return false;
        }
        if (
          field.showWhen &&
          !field.showWhen.equals.includes(String(values[field.showWhen.field] ?? ""))
        ) {
          return false;
        }
        if (field.showIf && !field.showIf(values)) return false;
        return true;
      }),
    [fields, values],
  );

  const hiddenFieldNames = useMemo(() => {
    const visible = new Set(visibleFields.map((f) => f.name));
    return fields.filter((f) => !visible.has(f.name)).map((f) => f.name);
  }, [fields, visibleFields]);

  const shortFields = useMemo(
    () => visibleFields.filter((f) => f.type !== "textarea"),
    [visibleFields],
  );
  const longFields = useMemo(
    () => visibleFields.filter((f) => f.type === "textarea"),
    [visibleFields],
  );

  const validate = () => {
    const next: Record<string, string> = {};
    visibleFields.forEach((field) => {
      const value = values[field.name];
      const stringValue =
        typeof value === "string" ? value.trim() : String(value ?? "");

      if (
        field.required &&
        (stringValue === "" || stringValue === FLEET_SELECT_NONE)
      ) {
        next[field.name] = `${field.label} is required`;
      }

      // Validate date format (YYYY-MM-DD) - DatePicker ensures this
      if (
        field.type === "date" &&
        stringValue &&
        stringValue !== FLEET_SELECT_NONE
      ) {
        const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
        if (!dateRegex.test(stringValue)) {
          next[field.name] = `${field.label} must be a valid date`;
        } else {
          const date = new Date(stringValue + "T00:00:00Z");
          if (isNaN(date.getTime())) {
            next[field.name] = `${field.label} is not a valid date`;
          } else if (field.dateBound === "future") {
            const startOfToday = new Date();
            startOfToday.setUTCHours(0, 0, 0, 0);
            if (date <= startOfToday) {
              next[field.name] = `${field.label} must be in the future`;
            }
          } else if (date > new Date()) {
            next[field.name] = `${field.label} cannot be in the future`;
          }
        }
      }

      // Format check (e.g. plate numbers). Skipped for an empty optional field —
      // "required" above already owns the empty case. Upper-cased to match the
      // server, which stores plates upper-case.
      if (field.pattern && stringValue && stringValue !== FLEET_SELECT_NONE) {
        const candidate = field.pattern.uppercase === false ? stringValue : stringValue.toUpperCase();
        if (!field.pattern.regex.test(candidate)) {
          next[field.name] = field.pattern.message;
        }
      }
    });
    setErrors(next);
    return Object.keys(next).length === 0;
  };

  // Field types keyed by name, so the submit payload can coerce each value to the
  // type the API expects (number columns come back from the API as strings like
  // "24.00", which the DTO's @IsNumber rejects on an otherwise-unchanged save).
  const fieldTypeByName = useMemo(() => {
    const map: Record<string, FleetFormFieldDef["type"]> = {};
    fields.forEach((f) => (map[f.name] = f.type));
    return map;
  }, [fields]);

  // Emptying one of these means "unset the column", so it submits an explicit
  // null instead of being dropped from the payload like other empty fields.
  const clearableByName = useMemo(() => {
    const map: Record<string, boolean> = {};
    fields.forEach((f) => (map[f.name] = Boolean(f.clearable)));
    return map;
  }, [fields]);

  const handleSubmit = () => {
    // Hard gate: a driver record cannot be saved until its identity is verified
    // with Fayda. Mirrored server-side in DriversService.
    if (verifyWithFayda && !faydaVerified) {
      setFaydaError("Verify the driver's identity with Fayda before saving.");
      return;
    }
    if (!validate()) return;
    // Derived fields are never edited, so form state for them can be stale (or
    // seeded from the record) — recompute before building the payload.
    const submitted: Record<string, unknown> = { ...values };
    fields.forEach((field) => {
      if (field.derivedValue) submitted[field.name] = field.derivedValue(values);
    });
    // A field the answers hid no longer applies to this record — send an explicit
    // null so the column is unset, instead of leaving a stale value behind.
    hiddenFieldNames.forEach((name) => {
      submitted[name] = null;
    });
    const payload = Object.fromEntries(
      Object.entries(submitted)
        // `_`-prefixed keys are form-local scratch written by `onOptionSelected`
        // (e.g. _hasTrailer, which drives visibility). The API validates with
        // forbidNonWhitelisted, so an undeclared key would 400 the whole save.
        .filter(([key]) => !key.startsWith("_"))
        .map(([key, value]) => {
          if (hiddenFieldNames.includes(key)) return [key, null];
          if (value === FLEET_SELECT_NONE || value === "" || value == null)
            return [key, clearableByName[key] ? null : undefined];
          if (fieldTypeByName[key] === "number") {
            const num = Number(value);
            return [key, Number.isNaN(num) ? undefined : num];
          }
          return [key, value];
        })
        .filter(([, value]) => value !== undefined),
    );
    onSubmit(payload);
  };

  const renderField = (field: FleetFormFieldDef) => {
    const value = values[field.name];
    const error = errors[field.name];
    // Fayda-owned identity fields (name/email/phone/DOB/gender) are populated
    // only by verification and never hand-edited.
    const isDisabled = Boolean(field.disabled || field.faydaLocked);

    // Computed from other fields (e.g. the import run implied by the export
    // run) — read-only, and recomputed here rather than read from form state.
    if (field.derivedValue) {
      return (
        <TextInput
          key={field.name}
          label={field.label}
          description={field.description}
          placeholder={field.placeholder}
          value={field.derivedValue(values)}
          readOnly
          variant="filled"
          error={error}
        />
      );
    }

    if (field.type === "radio") {
      return (
        <Radio.Group
          key={field.name}
          label={field.label}
          value={value == null ? "" : String(value)}
          onChange={(next) =>
            setValues((current) => ({ ...current, [field.name]: next }))
          }
          error={error}
        >
          <Group gap="lg" mt={6}>
            {(field.options ?? []).map((o) => (
              <Radio key={o.value} value={o.value} label={o.label} disabled={isDisabled} />
            ))}
          </Group>
        </Radio.Group>
      );
    }

    if (field.type === "select") {
      return (
        <Select
          key={field.name}
          label={field.label}
          description={field.description}
          placeholder={field.placeholder}
          data={field.options ?? []}
          value={
            value == null || value === ""
              ? field.noneOption
                ? FLEET_SELECT_NONE
                : null
              : String(value)
          }
          onChange={(next) =>
            setValues((current) => {
              const patch = field.onOptionSelected
                ? field.onOptionSelected(
                    field.options?.find((o) => o.value === next),
                    current,
                  )
                : {};
              return { ...current, [field.name]: next ?? "", ...patch };
            })
          }
          error={error}
          searchable
          clearable={field.clearable}
          disabled={selectOptionsLoading || isDisabled}
          rightSection={
            selectOptionsLoading ? (
              <Loader2 size={14} className="animate-spin" />
            ) : undefined
          }
        />
      );
    }

    if (field.type === "multiselect") {
      const arrayValue = Array.isArray(value)
        ? value
        : typeof value === "string" && value
          ? [value]
          : [];

      return (
        <MultiSelect
          key={field.name}
          label={field.label}
          placeholder={field.placeholder || "Select options"}
          data={field.options ?? []}
          value={arrayValue.map(String)}
          onChange={(next) =>
            setValues((current) => ({ ...current, [field.name]: next }))
          }
          error={error}
          searchable
          clearable
          disabled={selectOptionsLoading || isDisabled}
          rightSection={
            selectOptionsLoading ? (
              <Loader2 size={14} className="animate-spin" />
            ) : undefined
          }
        />
      );
    }

    if (field.type === "number") {
      return (
        <NumberInput
          key={field.name}
          label={field.label}
          placeholder={field.placeholder}
          value={value === "" || value == null ? "" : Number(value)}
          onChange={(next) =>
            setValues((current) => ({
              ...current,
              [field.name]: next === "" ? "" : next,
            }))
          }
          error={error}
          disabled={isDisabled}
        />
      );
    }

    if (field.type === "textarea") {
      return (
        <Textarea
          key={field.name}
          label={field.label}
          placeholder={field.placeholder}
          value={String(value ?? "")}
          onChange={(e) =>
            setValues((current) => ({
              ...current,
              [field.name]: e.currentTarget?.value,
            }))
          }
          error={error}
          minRows={3}
          disabled={isDisabled}
        />
      );
    }

    if (field.type === "date") {
      return (
        <TextInput
          key={field.name}
          type="date"
          label={field.label}
          placeholder={field.placeholder || "YYYY-MM-DD"}
          value={typeof value === "string" ? value.slice(0, 10) : ""}
          onChange={(e) =>
            setValues((current) => ({
              ...current,
              [field.name]: e.currentTarget?.value ?? "",
            }))
          }
          error={error}
          disabled={isDisabled}
          description={field.description || "Select a date"}
          size="sm"
          radius="md"
          styles={{
            input: {
              borderColor: "var(--mantine-color-gray-3)",
              backgroundColor: "var(--mantine-color-gray-0)",
              transition: "all 200ms ease",
              "&:focus": {
                borderColor: "var(--mantine-color-green-5)",
                backgroundColor: "var(--mantine-color-gray-1)",
              },
            },
          }}
        />
      );
    }

    return (
      <TextInput
        key={field.name}
        label={field.label}
        placeholder={field.placeholder}
        description={field.description}
        value={String(value ?? "")}
        onChange={(e) =>
          setValues((current) => ({
            ...current,
            [field.name]: e.currentTarget?.value,
          }))
        }
        error={error}
        disabled={isDisabled}
      />
    );
  };

  return (
    <Modal
      opened={open}
      onClose={() => onOpenChange(false)}
      title={<Text fw={600}>{title}</Text>}
      size="lg"
      radius="lg"
      centered
    >
      <Stack gap="md">
        {verifyWithFayda && (
          <Group justify="space-between" wrap="nowrap">
            {faydaVerified ? (
              <Badge
                color="green"
                variant="light"
                size="lg"
                leftSection={<ShieldCheck size={14} />}
              >
                Identity verified with Fayda
              </Badge>
            ) : (
              <Text size="sm" c="dimmed">
                Identity must be verified with Fayda before this driver can be
                saved.
              </Text>
            )}
            <Button
              variant={faydaVerified ? "default" : "light"}
              color="edr-green"
              size="xs"
              leftSection={<ShieldCheck size={14} />}
              loading={faydaLoading}
              onClick={handleFaydaVerify}
            >
              {faydaVerified ? "Re-verify" : "Verify with Fayda"}
            </Button>
          </Group>
        )}
        {verifyWithFayda && faydaError && (
          <Alert color="red" variant="light">
            {faydaError}
          </Alert>
        )}
        <SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
          {shortFields.map(renderField)}
        </SimpleGrid>
        {longFields.map(renderField)}
        <Group justify="flex-end" gap="sm" mt="sm">
          <Button variant="default" onClick={() => onOpenChange(false)}>
            Cancel
          </Button>
          <Button
            color="edr-green"
            loading={isSubmitting}
            onClick={handleSubmit}
            disabled={verifyWithFayda && !faydaVerified}
          >
            Save
          </Button>
        </Group>
      </Stack>
    </Modal>
  );
};

export default FleetFormDialog;
