import { Link } from '@inertiajs/react';
import { ShieldCheck } from 'lucide-react';
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { Combobox, type ComboboxOption } from '@/components/crm/combobox';
import { FormField, NativeSelect } from '@/components/crm/native-select';
import { Callout } from '@/components/crm/page-header';
import { show as customerShow } from '@/routes/customers';
import { customers as customersLookup, sites as sitesLookup } from '@/routes/lookups';

/**
 * One installation site of a customer, as the site picker needs it.
 */
export type CustomerSiteOption = {
    id: number;
    label: string | null;
    address: string;
    state: string;
    state_label: string;
    requires_independent_inspection: boolean;
};

/**
 * The customer a form was opened for, with the sites that belong to them.
 *
 * The page only ever carries the customer it was pointed at — never the whole
 * register. Everybody else is found by typing.
 */
export type CustomerOption = {
    id: number;
    name: string;
    customer_number: string;
    sites: CustomerSiteOption[];
};

type SiteLookupOption = {
    value: number;
    label: string | null;
    address: string;
    state: string;
    state_label: string;
    requires_independent_inspection: boolean;
};

type Props = {
    customers: CustomerOption[];
    customerId: number | null;
    siteId: number | null;
    /** Both ids travel together: choosing a customer chooses their first site. */
    onChange: (customerId: number | null, siteId: number | null) => void;
    errors?: { customer_id?: string; site_id?: string };
    /** A link or note shown under the customer field, such as "open the record". */
    customerFooter?: ReactNode;
};

/**
 * The customer and site pair every job form starts with.
 *
 * There are thousands of customers and more sites than that, so neither list is
 * sent with the page: the customer picker searches the register on request, and
 * the site list is asked for only once a customer has been chosen. That is what
 * keeps "new job" opening at all on a database this size.
 *
 * The customer named on the page is the one already chosen — opened from a
 * customer's page, or the existing customer when editing a job. Their sites
 * arrive with it, and the rest are fetched as soon as a different customer is
 * picked.
 */
export function CustomerSiteFields({
    customers,
    customerId,
    siteId,
    onChange,
    errors = {},
    customerFooter,
}: Props) {
    const [options, setOptions] = useState<ComboboxOption[]>(() =>
        customers.map((customer) => ({
            value: customer.id,
            label: customer.name,
            note: customer.customer_number,
        })),
    );

    const [sites, setSites] = useState<CustomerSiteOption[]>(
        () =>
            customers.find((customer) => customer.id === customerId)?.sites ??
            [],
    );

    const [loading, setLoading] = useState(false);
    const [failure, setFailure] = useState<string | null>(null);

    // The fetch below runs when the customer changes, so it reads the current
    // site and the current callback rather than closing over stale ones.
    const siteRef = useRef(siteId);
    const changeRef = useRef(onChange);

    siteRef.current = siteId;
    changeRef.current = onChange;

    useEffect(() => {
        if (customerId === null) {
            setSites([]);
            setLoading(false);
            setFailure(null);

            return;
        }

        let cancelled = false;

        setLoading(true);
        setFailure(null);

        fetch(sitesLookup({ query: { customer: customerId } }).url, {
            headers: {
                Accept: 'application/json',
                'X-Requested-With': 'XMLHttpRequest',
            },
            credentials: 'same-origin',
        })
            .then((response) => {
                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}`);
                }

                return response.json() as Promise<{
                    options: SiteLookupOption[];
                }>;
            })
            .then((payload) => {
                if (cancelled) {
                    return;
                }

                const found: CustomerSiteOption[] = payload.options.map(
                    (site) => ({
                        id: site.value,
                        label: site.label,
                        address: site.address,
                        state: site.state,
                        state_label: site.state_label,
                        requires_independent_inspection:
                            site.requires_independent_inspection,
                    }),
                );

                setSites(found);

                // The site already chosen stays chosen when it belongs to this
                // customer; otherwise the first one becomes the install
                // address, which is the one people mean nine times in ten.
                if (!found.some((site) => site.id === siteRef.current)) {
                    changeRef.current(customerId, found[0]?.id ?? null);
                }
            })
            .catch(() => {
                if (cancelled) {
                    return;
                }

                // A failed request is not "this customer has no sites", so it
                // says so rather than letting someone create a job against the
                // wrong address.
                setSites([]);
                setFailure(
                    'The sites for that customer could not be loaded. Check the connection and choose the customer again.',
                );
            })
            .finally(() => {
                if (!cancelled) {
                    setLoading(false);
                }
            });

        return () => {
            cancelled = true;
        };
    }, [customerId]);

    const handleCustomer = (
        nextId: number | null,
        option: ComboboxOption | null,
    ) => {
        // A customer found by searching is kept in the list, so the field shows
        // the name that was chosen rather than going blank behind it.
        if (option !== null) {
            setOptions((previous) =>
                previous.some((known) => known.value === option.value)
                    ? previous
                    : [option, ...previous],
            );
        }

        onChange(nextId, null);
    };

    const selectedSite = sites.find((site) => site.id === siteId) ?? null;

    return (
        <>
            <div className="grid gap-x-6 gap-y-4 md:grid-cols-2">
                <FormField
                    label="Customer"
                    htmlFor="customer_id"
                    error={errors.customer_id}
                    required
                >
                    <Combobox
                        id="customer_id"
                        name="customer_id"
                        value={customerId}
                        onChange={handleCustomer}
                        options={options}
                        remote={{ url: customersLookup().url }}
                        placeholder="Search customers"
                        emptyLabel="No customer matches that"
                    />
                    {customerFooter ?? null}
                </FormField>

                <FormField
                    label="Installation site"
                    htmlFor="site_id"
                    error={errors.site_id}
                    hint={
                        selectedSite
                            ? `${selectedSite.state_label}${
                                  selectedSite.requires_independent_inspection
                                      ? ' — Victoria requires an independent Licensed Electrical Inspector to pass the installation before it can be energised.'
                                      : ''
                              }`
                            : null
                    }
                    required
                >
                    <NativeSelect
                        id="site_id"
                        name="site_id"
                        value={siteId === null ? '' : String(siteId)}
                        onChange={(event) =>
                            onChange(
                                customerId,
                                event.target.value === ''
                                    ? null
                                    : Number(event.target.value),
                            )
                        }
                        required
                        disabled={sites.length === 0}
                    >
                        <option value="">
                            {loading
                                ? 'Loading sites…'
                                : sites.length === 0
                                  ? 'No sites for this customer'
                                  : 'Select a site'}
                        </option>
                        {sites.map((site) => (
                            <option key={site.id} value={String(site.id)}>
                                {site.label ? `${site.label} — ` : ''}
                                {site.address}
                            </option>
                        ))}
                    </NativeSelect>
                </FormField>
            </div>

            {failure ? (
                <Callout
                    tone="warning"
                    className="mt-4"
                    title="The sites could not be loaded"
                >
                    {failure}
                </Callout>
            ) : null}

            {!loading && failure === null && customerId !== null && sites.length === 0 ? (
                <Callout
                    tone="warning"
                    className="mt-4"
                    title="This customer has no installation site"
                >
                    Add one on the{' '}
                    <Link
                        href={customerShow(customerId)}
                        className="underline"
                    >
                        customer page
                    </Link>{' '}
                    before creating the job.
                </Callout>
            ) : null}

            {selectedSite?.requires_independent_inspection ? (
                <Callout
                    tone="info"
                    icon={ShieldCheck}
                    className="mt-4"
                    title={`${selectedSite.state_label} rules`}
                >
                    A prescribed Certificate of Electrical Safety and an
                    independent inspection are required before the system can be
                    energised. The job cannot complete until that inspection is
                    recorded as passed.
                </Callout>
            ) : null}
        </>
    );
}
