import { Head, router, useForm } from '@inertiajs/react';
import {
    Check,
    Copy,
    ExternalLink,
    Eye,
    EyeOff,
    KeyRound,
    Lock,
    Pencil,
    Plus,
    ShieldCheck,
    Trash2,
    X,
} from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { Field } from '@/components/crm/form-field';
import { StatStrip } from '@/components/crm/kpi-tile';
import { FormField, NativeSelect } from '@/components/crm/native-select';
import { PageHeader, SectionCard } from '@/components/crm/page-header';
import { StatusPill } from '@/components/crm/status-badge';
import { toneClasses } from '@/components/crm/tone';
import {
    DataPagination,
    FilterBar,
    FilterField,
    SearchInput,
} from '@/components/crm/toolbar';
import { Button } from '@/components/ui/button';
import {
    Dialog,
    DialogContent,
    DialogDescription,
    DialogHeader,
    DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { formatDateTime, formatRelative } from '@/lib/format';
import { cn } from '@/lib/utils';
import {
    destroy as destroyVaultEntry,
    index as vaultIndex,
    reveal as revealVaultEntry,
    store as storeVaultEntry,
    update as updateVaultEntry,
} from '@/routes/vault';
import type { VaultEntryRow, VaultIndexProps } from '@/types';

const textareaClassName =
    'border-input dark:bg-input/30 flex min-h-[84px] w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]';

/** How long a revealed password stays on screen before hiding itself again. */
const REVEAL_SECONDS = 60;

/**
 * The CSRF token Laravel writes into a readable cookie. The reveal is a POST
 * made with `fetch` rather than an Inertia visit, because the answer is one
 * value rather than a page.
 */
function csrfToken(): string {
    const match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/);

    return match ? decodeURIComponent(match[1]) : '';
}

export default function Vault({
    entries,
    filters,
    filterOptions,
    counts,
    can,
}: VaultIndexProps) {
    const [search, setSearch] = useState(filters.search ?? '');
    const [editing, setEditing] = useState<VaultEntryRow | null>(null);
    const [creating, setCreating] = useState(false);

    const applyFilters = (next: {
        search?: string;
        status?: string;
        password?: string;
    }) => {
        router.get(
            vaultIndex().url,
            {
                search: next.search ?? filters.search ?? '',
                status: next.status ?? filters.status,
                password: next.password ?? filters.password,
            },
            { preserveState: true, preserveScroll: true, replace: true },
        );
    };

    const submitSearch = (event: React.FormEvent) => {
        event.preventDefault();
        applyFilters({ search });
    };

    return (
        <>
            <Head title="Vault" />

            <div className="flex flex-col gap-5">
                <PageHeader
                    eyebrow={
                        <>
                            <KeyRound className="size-3.5 text-solar" />
                            <span>Vault</span>
                        </>
                    }
                    title="Monitoring credentials"
                    description="The logins we hold for the portals our systems report to — Sungrow, Fronius, Tesla and the rest. Admin and operations only. The password is stored encrypted and is only read one entry at a time, which is recorded."
                    actions={
                        can.manage ? (
                            <Button size="sm" onClick={() => setCreating(true)}>
                                <Plus className="size-4" />
                                Add credentials
                            </Button>
                        ) : undefined
                    }
                />

                <StatStrip
                    items={[
                        {
                            label: 'Portals in use',
                            value: counts.active,
                            tone: 'solar',
                            icon: KeyRound,
                        },
                        {
                            label: 'Need a password',
                            value: counts.missingPassword,
                            tone: counts.missingPassword > 0 ? 'warning' : 'success',
                            icon: Lock,
                        },
                        {
                            label: 'Retired',
                            value: counts.retired,
                            tone: 'slate',
                            icon: ShieldCheck,
                        },
                    ]}
                />

                {counts.missingPassword > 0 ? (
                    <div className="bg-warning-soft text-warning flex flex-wrap items-center justify-between gap-3 rounded-xl px-4 py-3 text-sm ring-1 ring-warning/25">
                        <p className="flex items-center gap-2">
                            <Lock className="size-4 shrink-0" />
                            <span>
                                {counts.missingPassword === 1
                                    ? 'One entry has no password recorded yet.'
                                    : `${counts.missingPassword} entries have no password recorded yet.`}{' '}
                                They came across from the old system, which
                                encrypted them with a key this one cannot read.
                            </span>
                        </p>
                        <Button
                            size="sm"
                            variant="outline"
                            onClick={() => applyFilters({ password: 'missing' })}
                            data-test="filter-missing-passwords"
                        >
                            Show them
                        </Button>
                    </div>
                ) : null}

                <FilterBar>
                    <form onSubmit={submitSearch} className="flex flex-1 gap-2">
                        <SearchInput
                            value={search}
                            onChange={setSearch}
                            placeholder="Portal, login, organisation code or note"
                        />
                        <Button type="submit" variant="outline" size="sm">
                            Search
                        </Button>
                    </form>

                    <FilterField label="Show">
                        <NativeSelect
                            value={filters.status}
                            onChange={(event) =>
                                applyFilters({ status: event.target.value })
                            }
                            className="h-9 w-auto"
                        >
                            {filterOptions.statuses.map((option) => (
                                <option key={option.value} value={option.value}>
                                    {option.label}
                                </option>
                            ))}
                        </NativeSelect>
                    </FilterField>

                    <FilterField label="Password">
                        <NativeSelect
                            value={filters.password}
                            onChange={(event) =>
                                applyFilters({ password: event.target.value })
                            }
                            className="h-9 w-auto"
                        >
                            {filterOptions.passwords.map((option) => (
                                <option key={option.value} value={option.value}>
                                    {option.label}
                                </option>
                            ))}
                        </NativeSelect>
                    </FilterField>

                    {filters.search || filters.status !== 'active' || filters.password !== 'any' ? (
                        <Button
                            size="sm"
                            variant="ghost"
                            onClick={() => {
                                setSearch('');
                                router.get(
                                    vaultIndex().url,
                                    {},
                                    { preserveState: true, preserveScroll: true },
                                );
                            }}
                        >
                            <X className="size-3.5" />
                            Clear
                        </Button>
                    ) : null}
                </FilterBar>

                {entries.data.length === 0 ? (
                    <SectionCard
                        title="Nothing in the vault"
                        description="Nothing matched, or the list is empty."
                        icon={KeyRound}
                    >
                        <p className="text-sm text-muted-foreground">
                            {filters.search
                                ? `No portal matches “${filters.search}”.`
                                : counts.total === 0
                                  ? 'The vault is empty. Add the first portal, or import the old list with vault:import-legacy.'
                                  : 'Nothing matches these filters.'}
                        </p>
                    </SectionCard>
                ) : (
                    <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
                        {entries.data.map((entry) => (
                            <EntryCard
                                key={entry.id}
                                entry={entry}
                                can={can}
                                onEdit={() => setEditing(entry)}
                            />
                        ))}
                    </div>
                )}

                <div className="bg-card shadow-card overflow-hidden rounded-xl ring-1 ring-hairline">
                    <DataPagination page={entries} noun="portals" />
                </div>

                <p className="flex items-start gap-2 text-xs text-muted-foreground">
                    <ShieldCheck className="mt-0.5 size-3.5 shrink-0" />
                    Passwords are encrypted with this installation's key, which
                    is why a database dump on its own gives nothing away. Copy
                    one only when it is going into the portal or to the crew who
                    needs it, and record a password change here the moment the
                    portal applies it.
                </p>
            </div>

            {can.manage ? (
                <VaultEntryDialog
                    open={creating || editing !== null}
                    entry={editing}
                    onClose={() => {
                        setCreating(false);
                        setEditing(null);
                    }}
                />
            ) : null}
        </>
    );
}

/**
 * One portal, with its login, organisation code and the password behind a
 * reveal. The audit line at the bottom is the point of the card: this is the
 * one screen where somebody can see what a colleague has opened.
 */
function EntryCard({
    entry,
    can,
    onEdit,
}: {
    entry: VaultEntryRow;
    can: VaultIndexProps['can'];
    onEdit: () => void;
}) {
    const [revealed, setRevealed] = useState<string | null>(null);
    const [busy, setBusy] = useState(false);
    const [error, setError] = useState<string | null>(null);
    const [copied, setCopied] = useState<string | null>(null);

    // A password left on screen is a password left on screen. It hides itself
    // after a minute, and taking another look is one click.
    const timer = useRef<number | null>(null);
    useEffect(
        () => () => {
            if (timer.current !== null) {
                window.clearTimeout(timer.current);
            }
        },
        [],
    );

    const remember = (key: string) => {
        setCopied(key);
        window.setTimeout(() => setCopied(null), 1500);
    };

    const copy = async (value: string, key: string) => {
        try {
            await navigator.clipboard.writeText(value);
            remember(key);
        } catch {
            setError('The browser would not allow a copy. Reveal it and select the value.');
        }
    };

    /**
     * Read the password from the server. Returns it as well as showing it, so
     * the copy button can reuse the same request instead of making a second
     * one — and so only one read is recorded.
     */
    const reveal = async (): Promise<string | null> => {
        setBusy(true);
        setError(null);

        try {
            const response = await fetch(revealVaultEntry(entry.id).url, {
                method: 'POST',
                credentials: 'same-origin',
                headers: {
                    Accept: 'application/json',
                    'X-XSRF-TOKEN': csrfToken(),
                    'X-Requested-With': 'XMLHttpRequest',
                },
            });

            if (!response.ok) {
                if (response.status === 409) {
                    const payload = (await response
                        .json()
                        .catch(() => null)) as { message?: string } | null;

                    throw new Error(
                        payload?.message ??
                            'This password was encrypted by a different installation.',
                    );
                }

                throw new Error(
                    response.status === 404
                        ? 'No password has been stored for this entry yet.'
                        : 'The password could not be read.',
                );
            }

            const data = (await response.json()) as { password?: string };
            setRevealed(data.password ?? '');

            if (timer.current !== null) {
                window.clearTimeout(timer.current);
            }

            timer.current = window.setTimeout(
                () => setRevealed(null),
                REVEAL_SECONDS * 1000,
            );

            return data.password ?? '';
        } catch (exception) {
            setError(
                exception instanceof Error
                    ? exception.message
                    : 'The password could not be read.',
            );

            return null;
        } finally {
            setBusy(false);
        }
    };

    const copyPassword = async () => {
        const value = revealed ?? (await reveal());

        if (value) {
            await copy(value, 'password');
        }
    };

    const remove = () => {
        if (
            !window.confirm(
                `Remove ${entry.title} from the vault? It is kept on the database, so it can be brought back.`,
            )
        ) {
            return;
        }

        router.delete(destroyVaultEntry(entry.id).url, { preserveScroll: true });
    };

    return (
        <article
            id={`entry-${entry.id}`}
            className={cn(
                'bg-card shadow-card flex flex-col rounded-xl ring-1 ring-hairline transition-shadow scroll-mt-24 hover:shadow-lg',
                !entry.is_active && 'opacity-80',
            )}
        >
            <header className="flex items-start gap-3 px-4 pt-4">
                <span
                    className={cn(
                        'flex size-9 shrink-0 items-center justify-center rounded-lg',
                        toneClasses(entry.is_active ? 'solar' : 'slate').soft,
                    )}
                >
                    <KeyRound className="size-4" />
                </span>

                <div className="min-w-0 flex-1">
                    <div className="flex flex-wrap items-center gap-1.5">
                        <h3 className="truncate text-sm font-semibold">
                            {entry.title}
                        </h3>
                        {entry.is_active ? null : (
                            <StatusPill tone="slate">Retired</StatusPill>
                        )}
                    </div>
                    <p className="truncate text-xs text-muted-foreground">
                        {entry.host ?? 'No address recorded'}
                    </p>
                </div>

                {can.manage ? (
                    <div className="flex shrink-0 items-center gap-0.5">
                        <button
                            type="button"
                            onClick={onEdit}
                            aria-label={`Edit ${entry.title}`}
                            className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
                        >
                            <Pencil className="size-3.5" />
                        </button>
                        <button
                            type="button"
                            onClick={remove}
                            aria-label={`Remove ${entry.title}`}
                            className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
                        >
                            <Trash2 className="size-3.5" />
                        </button>
                    </div>
                ) : null}
            </header>

            <dl className="mt-3 flex flex-1 flex-col gap-2 px-4 pb-3">
                <CredentialRow
                    label="Login"
                    value={entry.username}
                    empty="No login recorded"
                    copied={copied === 'username'}
                    onCopy={() =>
                        entry.username && copy(entry.username, 'username')
                    }
                />

                <CredentialRow
                    label="Organisation code"
                    value={entry.org_code}
                    empty="Not needed for this portal"
                    copied={copied === 'org'}
                    onCopy={() => entry.org_code && copy(entry.org_code, 'org')}
                />

                <div className="rounded-md bg-muted/40 px-2.5 py-2 ring-1 ring-hairline">
                    <div className="flex items-center justify-between gap-2">
                        <span className="crm-label">Password</span>

                        {entry.has_password ? (
                            <div className="flex items-center gap-1">
                                <button
                                    type="button"
                                    onClick={() =>
                                        revealed === null
                                            ? void reveal()
                                            : setRevealed(null)
                                    }
                                    disabled={busy}
                                    data-test={`reveal-${entry.id}`}
                                    className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] font-medium text-solar-ink transition-colors hover:bg-solar-soft disabled:opacity-50"
                                >
                                    {revealed === null ? (
                                        <>
                                            <Eye className="size-3" />
                                            {busy ? 'Opening…' : 'Reveal'}
                                        </>
                                    ) : (
                                        <>
                                            <EyeOff className="size-3" />
                                            Hide
                                        </>
                                    )}
                                </button>
                                <button
                                    type="button"
                                    onClick={() => void copyPassword()}
                                    className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
                                >
                                    {copied === 'password' ? (
                                        <Check className="size-3" />
                                    ) : (
                                        <Copy className="size-3" />
                                    )}
                                    Copy
                                </button>
                            </div>
                        ) : can.manage ? (
                            <button
                                type="button"
                                onClick={onEdit}
                                className="rounded px-1.5 py-0.5 text-[11px] font-medium text-warning hover:underline"
                            >
                                Add the password
                            </button>
                        ) : null}
                    </div>

                    <p className="mt-1 font-mono text-xs break-all">
                        {entry.has_password
                            ? (revealed ?? '••••••••••••')
                            : 'Not recorded yet'}
                    </p>

                    {error ? (
                        <p className="mt-1 text-[11px] text-destructive">
                            {error}
                        </p>
                    ) : null}
                    {revealed !== null ? (
                        <p className="mt-1 text-[10px] text-muted-foreground">
                            Hides itself in {REVEAL_SECONDS} seconds. Reading it
                            was recorded against your name.
                        </p>
                    ) : null}
                </div>

                {entry.notes ? (
                    <p className="line-clamp-3 text-xs leading-relaxed text-muted-foreground">
                        {entry.notes}
                    </p>
                ) : null}
            </dl>

            <footer className="border-hairline flex flex-wrap items-center justify-between gap-2 border-t px-4 py-2.5 text-[11px] text-muted-foreground">
                <span
                    title={
                        entry.last_revealed_at
                            ? formatDateTime(entry.last_revealed_at)
                            : undefined
                    }
                >
                    {entry.reveal_count === 0
                        ? 'Never opened'
                        : `Read ${entry.reveal_count}× · ${
                              entry.last_revealed_by ?? 'somebody since removed'
                          } ${formatRelative(entry.last_revealed_at)}`}
                </span>

                {entry.url ? (
                    <a
                        href={entry.url}
                        target="_blank"
                        rel="noreferrer"
                        className="inline-flex items-center gap-1 font-medium text-solar-ink underline-offset-2 hover:underline"
                    >
                        Open portal
                        <ExternalLink className="size-3" />
                    </a>
                ) : null}
            </footer>
        </article>
    );
}

/** A labelled value with a copy button, for the login and the org code. */
function CredentialRow({
    label,
    value,
    empty,
    copied,
    onCopy,
}: {
    label: string;
    value: string | null;
    empty: string;
    copied: boolean;
    onCopy: () => void;
}) {
    return (
        <div className="flex items-center justify-between gap-2">
            <div className="min-w-0">
                <dt className="crm-label">{label}</dt>
                <dd
                    className={cn(
                        'truncate font-mono text-xs',
                        value ? 'text-foreground' : 'text-muted-foreground',
                    )}
                    title={value ?? undefined}
                >
                    {value ?? empty}
                </dd>
            </div>

            {value ? (
                <button
                    type="button"
                    onClick={onCopy}
                    aria-label={`Copy ${label.toLowerCase()}`}
                    className="shrink-0 rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
                >
                    {copied ? (
                        <Check className="size-3.5 text-success" />
                    ) : (
                        <Copy className="size-3.5" />
                    )}
                </button>
            ) : null}
        </div>
    );
}

/**
 * Adding or changing an entry.
 *
 * The password field is blank when editing and says plainly that blank keeps
 * what is stored — this is not a screen where an empty box should silently
 * erase the only copy of a login.
 */
function VaultEntryDialog({
    open,
    entry,
    onClose,
}: {
    open: boolean;
    entry: VaultEntryRow | null;
    onClose: () => void;
}) {
    const form = useForm({
        title: entry?.title ?? '',
        url: entry?.url ?? '',
        username: entry?.username ?? '',
        org_code: entry?.org_code ?? '',
        password: '',
        notes: entry?.notes ?? '',
        is_active: entry?.is_active ?? true,
    });

    // Each time the dialog opens for a different entry, the fields start from
    // that entry rather than whatever was typed last time.
    useEffect(() => {
        if (!open) {
            return;
        }

        form.setData({
            title: entry?.title ?? '',
            url: entry?.url ?? '',
            username: entry?.username ?? '',
            org_code: entry?.org_code ?? '',
            password: '',
            notes: entry?.notes ?? '',
            is_active: entry?.is_active ?? true,
        });
        form.clearErrors();
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [open, entry?.id]);

    const submit = (event: React.FormEvent) => {
        event.preventDefault();

        if (entry) {
            form.put(updateVaultEntry(entry.id).url, {
                preserveScroll: true,
                onSuccess: onClose,
            });

            return;
        }

        form.post(storeVaultEntry().url, {
            preserveScroll: true,
            onSuccess: () => {
                form.reset();
                onClose();
            },
        });
    };

    return (
        <Dialog open={open} onOpenChange={(next) => (next ? null : onClose())}>
            <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-xl">
                <DialogHeader>
                    <DialogTitle>
                        {entry ? `Edit ${entry.title}` : 'Add monitoring credentials'}
                    </DialogTitle>
                    <DialogDescription>
                        One set of logins for one portal. The password is stored
                        encrypted and only ever read on request.
                    </DialogDescription>
                </DialogHeader>

                <form onSubmit={submit} className="flex flex-col gap-4">
                    <Field
                        label="Portal"
                        htmlFor="vault-title"
                        error={form.errors.title}
                        required
                        hint="What the card is called — the brand is enough."
                    >
                        <Input
                            id="vault-title"
                            value={form.data.title}
                            onChange={(event) =>
                                form.setData('title', event.target.value)
                            }
                            placeholder="Sungrow iSolarCloud"
                            required
                            autoComplete="off"
                        />
                    </Field>

                    <Field
                        label="Address"
                        htmlFor="vault-url"
                        error={form.errors.url}
                        hint="Paste the login page. https:// is added if you leave it off."
                    >
                        <Input
                            id="vault-url"
                            value={form.data.url}
                            onChange={(event) =>
                                form.setData('url', event.target.value)
                            }
                            placeholder="auweb3.isolarcloud.com"
                            autoComplete="off"
                        />
                    </Field>

                    <div className="grid gap-4 sm:grid-cols-2">
                        <Field
                            label="Login"
                            htmlFor="vault-username"
                            error={form.errors.username}
                        >
                            <Input
                                id="vault-username"
                                value={form.data.username}
                                onChange={(event) =>
                                    form.setData('username', event.target.value)
                                }
                                placeholder="techsupport@…"
                                autoComplete="off"
                            />
                        </Field>

                        <Field
                            label="Organisation code"
                            htmlFor="vault-org-code"
                            error={form.errors.org_code}
                            hint="Only some portals ask for one."
                        >
                            <Input
                                id="vault-org-code"
                                value={form.data.org_code}
                                onChange={(event) =>
                                    form.setData('org_code', event.target.value)
                                }
                                placeholder="AUS6ZYA9"
                                autoComplete="off"
                            />
                        </Field>
                    </div>

                    <Field
                        label="Password"
                        htmlFor="vault-password"
                        error={form.errors.password}
                        hint={
                            entry
                                ? 'Leave blank to keep the password already stored. Filling it in replaces it.'
                                : 'Leave blank if it is not to hand; the card will say so until it is.'
                        }
                    >
                        <Input
                            id="vault-password"
                            type="password"
                            value={form.data.password}
                            onChange={(event) =>
                                form.setData('password', event.target.value)
                            }
                            placeholder={entry ? '••••••••' : 'Paste the password'}
                            autoComplete="new-password"
                        />
                    </Field>

                    <FormField
                        label="Notes"
                        htmlFor="vault-notes"
                        error={form.errors.notes}
                        hint="Region, account manager, what the login is used for."
                    >
                        <textarea
                            id="vault-notes"
                            value={form.data.notes}
                            onChange={(event) =>
                                form.setData('notes', event.target.value)
                            }
                            rows={3}
                            className={textareaClassName}
                        />
                    </FormField>

                    <label className="flex items-center gap-2 text-sm">
                        <input
                            type="checkbox"
                            checked={form.data.is_active}
                            onChange={(event) =>
                                form.setData('is_active', event.target.checked)
                            }
                            className="accent-solar size-4"
                        />
                        Still in use
                        <span className="text-xs text-muted-foreground">
                            Untick to keep it on file without listing it as a
                            portal we use.
                        </span>
                    </label>

                    <div className="flex items-center justify-end gap-2 pt-1">
                        <Button
                            type="button"
                            variant="ghost"
                            onClick={onClose}
                            disabled={form.processing}
                        >
                            Cancel
                        </Button>
                        <Button type="submit" disabled={form.processing}>
                            <KeyRound className="size-4" />
                            {form.processing
                                ? 'Saving…'
                                : entry
                                  ? 'Save changes'
                                  : 'Add to the vault'}
                        </Button>
                    </div>
                </form>
            </DialogContent>
        </Dialog>
    );
}

Vault.layout = {
    breadcrumbs: [{ title: 'Vault', href: vaultIndex().url }],
};
