import { Head, router } from '@inertiajs/react';
import {
    AlertTriangle,
    CheckCircle2,
    FileWarning,
    FolderLock,
    ScanSearch,
    ShieldAlert,
    ShieldCheck,
    TriangleAlert,
} from 'lucide-react';
import { useState } from 'react';
import {
    Callout,
    EmptyState,
    KeyValueGrid,
    SectionCard,
} from '@/components/crm/page-header';
import { StatusPill } from '@/components/crm/status-badge';
import { Button } from '@/components/ui/button';
import {
    Dialog,
    DialogContent,
    DialogDescription,
    DialogFooter,
    DialogHeader,
    DialogTitle,
} from '@/components/ui/dialog';
import { Spinner } from '@/components/ui/spinner';
import { formatDateTime, formatRelative } from '@/lib/format';
import { cn } from '@/lib/utils';
import type { ScanFinding, ScanSeverity, SecurityScanProps } from '@/types';

/**
 * Settings -> Security scan.
 *
 * One screen, one question: is anything on this account sitting where it should
 * not be? The scan is read-only, so pressing the button costs nothing but the
 * few seconds it takes to read the files — what it finds is listed here with
 * the rule that caught it and the line it caught, and the files that are
 * certainly not supposed to be there can be moved out of the web root from
 * here, with a way back.
 *
 * Nothing on this page deletes. A held file keeps its bytes, so the worst a
 * wrong button press can do is take a file out of the site until somebody puts
 * it back.
 */

const severityTone: Record<ScanSeverity, string> = {
    critical: 'danger',
    high: 'warning',
    medium: 'slate',
};

const severityLabel: Record<ScanSeverity, string> = {
    critical: 'Critical',
    high: 'High',
    medium: 'Medium',
};

function FindingRow({ finding }: { finding: ScanFinding }) {
    return (
        <li className="border-t border-hairline px-5 py-3 first:border-t-0">
            <div className="flex flex-wrap items-center gap-2">
                <StatusPill tone={severityTone[finding.severity]}>
                    {severityLabel[finding.severity]}
                </StatusPill>
                <span className="min-w-0 flex-1 truncate font-mono text-xs">
                    {finding.relative}
                    {finding.line !== null ? `:${finding.line}` : ''}
                </span>
                <span className="rounded-md bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
                    {finding.rule}
                </span>
            </div>

            <p className="mt-1.5 text-xs leading-relaxed text-muted-foreground">
                {finding.reason}
            </p>

            {finding.evidence ? (
                <pre className="crm-scroll mt-2 overflow-x-auto rounded-lg bg-muted/60 px-3 py-2 font-mono text-[11px] leading-relaxed">
                    {finding.evidence}
                </pre>
            ) : null}
        </li>
    );
}

export default function SecurityScan({
    scan,
    targets,
    quarantine,
    can,
}: SecurityScanProps) {
    const [running, setRunning] = useState(false);
    const [holding, setHolding] = useState(false);
    const [confirming, setConfirming] = useState(false);

    const findings = scan?.findings ?? [];
    const clean = scan !== null && scan.total === 0;

    const run = () => {
        router.post(
            '/settings/security-scan',
            {},
            {
                preserveScroll: true,
                onStart: () => setRunning(true),
                onFinish: () => setRunning(false),
            },
        );
    };

    const hold = () => {
        setConfirming(false);

        router.post(
            '/settings/security-scan/quarantine',
            {},
            {
                preserveScroll: true,
                onStart: () => setHolding(true),
                onFinish: () => setHolding(false),
            },
        );
    };

    return (
        <>
            <Head title="Security scan" />

            <Callout
                tone={
                    scan === null
                        ? 'info'
                        : clean
                          ? 'success'
                          : (scan.counts.critical > 0
                                ? 'danger'
                                : 'warning') as string
                }
                icon={
                    scan === null
                        ? ScanSearch
                        : clean
                          ? ShieldCheck
                          : ShieldAlert
                }
                title={
                    scan === null
                        ? 'Nothing has been scanned yet'
                        : clean
                          ? 'Nothing matched any rule'
                          : `${scan.total} file${scan.total === 1 ? '' : 's'} matched a rule`
                }
            >
                {scan === null
                    ? 'A scan reads scripts, markup and server config for web shells, hidden payloads and scripts where they do not belong. It changes nothing on disk, so it is safe to run on a live site at any time.'
                    : `Last run ${formatRelative(scan.ran_at)}, by ${scan.ran_by}. The findings are listed below with the rule that caught each one; a scan never moves or deletes anything on its own.`}
            </Callout>

            <SectionCard
                title="Run a scan"
                description="Reads the whole account, not only this application, because planted code spreads sideways between the sites it shares a server with."
                icon={ScanSearch}
                tone="solar"
                actions={
                    can.run ? (
                        <Button onClick={run} disabled={running || holding}>
                            {running ? <Spinner /> : <ScanSearch className="size-4" />}
                            {running ? 'Scanning…' : 'Scan now'}
                        </Button>
                    ) : null
                }
            >
                <KeyValueGrid
                    columns={2}
                    items={[
                        {
                            label: 'What is read',
                            value: (
                                <span className="font-mono text-xs break-all">
                                    {targets.roots.join(', ') || '—'}
                                </span>
                            ),
                        },
                        {
                            label: 'Files larger than',
                            value: `${Math.round(targets.max_bytes / 1024 / 1024)} MB are skipped rather than parsed`,
                        },
                        {
                            label: 'Held automatically',
                            value: `${targets.threshold_label} and above, and never without confirmation`,
                        },
                        {
                            label: 'Dependencies, build output',
                            value: 'Vendor trees, node_modules, caches and backups are not read',
                        },
                    ]}
                />

                {running ? (
                    <div className="mt-4 flex items-start gap-3 rounded-xl bg-muted/50 px-4 py-3 ring-1 ring-hairline ring-inset">
                        <Spinner className="mt-0.5" />
                        <div className="min-w-0">
                            <p className="text-sm font-medium">
                                Reading the account…
                            </p>
                            <p className="mt-0.5 text-xs leading-relaxed text-muted-foreground">
                                Keep this page open until it finishes. Nothing
                                is being changed while it runs.
                            </p>
                        </div>
                    </div>
                ) : null}

                <p className="mt-4 text-xs leading-relaxed text-muted-foreground">
                    The same scan runs from the command line as{' '}
                    <code className="font-mono">php artisan crm:malware-scan</code>
                    , which is the one to schedule on the server. Whatever
                    either one finds, this page shows the last run.
                </p>
            </SectionCard>

            {scan === null ? (
                <SectionCard title="The report" flush>
                    <EmptyState
                        icon={FileWarning}
                        title="No report yet"
                        description="Press Scan now to read the account and write the first one. It takes a few seconds on a shared account."
                    />
                </SectionCard>
            ) : (
                <>
                    <SectionCard
                        title="What the last scan saw"
                        description={
                            scan.ran_at
                                ? `Ran ${formatDateTime(scan.ran_at)} (${formatRelative(scan.ran_at)}), started from ${scan.source === 'command' ? 'the command line' : 'this screen'}.`
                                : undefined
                        }
                        icon={ScanSearch}
                        tone="slate"
                    >
                        <KeyValueGrid
                            columns={4}
                            items={[
                                { label: 'Files read', value: scan.files.toLocaleString() },
                                { label: 'Skipped', value: scan.skipped.toLocaleString() },
                                { label: 'Took', value: `${scan.seconds.toFixed(1)}s` },
                                {
                                    label: 'Findings',
                                    value: `${scan.counts.critical} critical · ${scan.counts.high} high · ${scan.counts.medium} medium`,
                                },
                            ]}
                        />

                        <p className="mt-4 flex items-start gap-2 text-xs leading-relaxed text-muted-foreground">
                            <FolderLock className="mt-0.5 size-3.5 shrink-0" />
                            <span className="min-w-0 break-all">
                                {scan.roots.join(', ')}
                            </span>
                        </p>

                        {scan.errors.length > 0 ? (
                            <Callout
                                tone="warning"
                                icon={AlertTriangle}
                                title={`${scan.errors.length} place${scan.errors.length === 1 ? '' : 's'} could not be read`}
                                className="mt-4"
                            >
                                <p>
                                    A scan is only clean when it says so. Fix
                                    the permissions and run it again.
                                </p>
                                <ul className="mt-2 space-y-1 font-mono text-[11px]">
                                    {scan.errors.slice(0, 10).map((error) => (
                                        <li key={error} className="break-all">
                                            {error}
                                        </li>
                                    ))}
                                </ul>
                            </Callout>
                        ) : null}
                    </SectionCard>

                    <SectionCard
                        title="Findings"
                        description={
                            scan.total === 0
                                ? 'Every file that was read passed every rule.'
                                : `${scan.shown} of ${scan.total} shown, worst first. A finding is a reason to look at a file, not a verdict.`
                        }
                        icon={scan.total === 0 ? CheckCircle2 : TriangleAlert}
                        tone={scan.total === 0 ? 'success' : 'danger'}
                        flush
                    >
                        {scan.total === 0 ? (
                            <EmptyState
                                icon={ShieldCheck}
                                title="Nothing matched"
                                description="No shell, no encoded payload, no script in an upload folder and nothing injection-shaped."
                            />
                        ) : (
                            <ul className="crm-scroll max-h-[38rem] overflow-y-auto">
                                {findings.map((finding) => (
                                    <FindingRow
                                        key={`${finding.rule}-${finding.path}`}
                                        finding={finding}
                                    />
                                ))}
                            </ul>
                        )}
                    </SectionCard>

                    {scan.total > 0 ? (
                        <SectionCard
                            title="Hold the actionable files"
                            description={`Moves the ${targets.threshold_label} and above findings into quarantine, outside the web root, where nothing can ask for them over HTTP. Nothing is deleted.`}
                            icon={FolderLock}
                            tone="warning"
                            actions={
                                can.hold && scan.actionable > 0 ? (
                                    <Button
                                        variant="outline"
                                        onClick={() => setConfirming(true)}
                                        disabled={running || holding}
                                    >
                                        {holding ? (
                                            <Spinner />
                                        ) : (
                                            <FolderLock className="size-4" />
                                        )}
                                        {holding
                                            ? 'Holding…'
                                            : `Hold ${scan.actionable} file${scan.actionable === 1 ? '' : 's'}`}
                                    </Button>
                                ) : null
                            }
                        >
                            <KeyValueGrid
                                columns={2}
                                items={[
                                    {
                                        label: 'Could be held',
                                        value:
                                            scan.actionable === 0
                                                ? 'Nothing at or above the threshold'
                                                : `${scan.actionable} file${scan.actionable === 1 ? '' : 's'}`,
                                    },
                                    {
                                        label: 'Quarantine',
                                        value: (
                                            <span className="font-mono text-xs break-all">
                                                {quarantine.path}
                                                {quarantine.outside_public_root
                                                    ? ''
                                                    : ' (inside the public web root — move it)'}
                                            </span>
                                        ),
                                    },
                                    {
                                        label: 'Currently held',
                                        value: `${quarantine.held} file${quarantine.held === 1 ? '' : 's'}`,
                                    },
                                    {
                                        label: 'Put one back',
                                        value: (
                                            <span className="font-mono text-xs break-all">
                                                {quarantine.restore_command}
                                            </span>
                                        ),
                                    },
                                ]}
                            />

                            <p className="mt-4 text-xs leading-relaxed text-muted-foreground">
                                Each file is read again immediately before it is
                                moved, so anything that changed since the scan
                                stays where it is. A Medium finding is never
                                moved: it may be the office's own old code, and
                                that is a question for a person.
                            </p>

                            {quarantine.entries.length > 0 ? (
                                <div className="mt-4 border-t border-hairline pt-4">
                                    <p className="crm-label">
                                        Held files
                                    </p>
                                    <ul className="mt-2 space-y-1.5">
                                        {quarantine.entries.map((entry) => (
                                            <li
                                                key={entry.id}
                                                className="flex flex-wrap items-center gap-2 text-xs"
                                            >
                                                <span className="rounded-md bg-muted px-1.5 py-0.5 font-mono text-[10px]">
                                                    {entry.id}
                                                </span>
                                                <span className="min-w-0 flex-1 truncate font-mono">
                                                    {entry.original_path}
                                                </span>
                                                <span className="text-muted-foreground">
                                                    {entry.rule} ·{' '}
                                                    {formatRelative(
                                                        entry.held_at,
                                                    )}
                                                </span>
                                            </li>
                                        ))}
                                    </ul>
                                    {quarantine.held > quarantine.entries.length ? (
                                        <p className="mt-2 text-xs text-muted-foreground">
                                            …and{' '}
                                            {quarantine.held -
                                                quarantine.entries.length}{' '}
                                            more. List them all with{' '}
                                            <code className="font-mono">
                                                php artisan crm:quarantine
                                                --list
                                            </code>
                                            .
                                        </p>
                                    ) : null}
                                </div>
                            ) : null}
                        </SectionCard>
                    ) : null}
                </>
            )}

            <p
                className={cn(
                    'flex items-start gap-2 text-xs leading-relaxed text-muted-foreground',
                    'pb-2',
                )}
            >
                <ShieldCheck className="mt-0.5 size-3.5 shrink-0" />
                Settings → Backup &amp; Restore takes the copy that makes any of
                this reversible, and it is worth taking one before holding a
                batch of files.
            </p>

            <Dialog open={confirming} onOpenChange={setConfirming}>
                <DialogContent>
                    <DialogHeader>
                        <DialogTitle>
                            Move {scan?.actionable ?? 0} file
                            {(scan?.actionable ?? 0) === 1 ? '' : 's'} to
                            quarantine?
                        </DialogTitle>
                        <DialogDescription>
                            Each one is read again first, and only files at or
                            above {targets.threshold_label} are taken. They
                            leave the web root, so the site can no longer serve
                            them — and they keep their bytes, so the command
                            line can put any of them back. Nothing is deleted
                            here.
                        </DialogDescription>
                    </DialogHeader>

                    <DialogFooter>
                        <Button
                            variant="outline"
                            onClick={() => setConfirming(false)}
                        >
                            Cancel
                        </Button>
                        <Button onClick={hold} disabled={holding}>
                            {holding ? <Spinner /> : <FolderLock className="size-4" />}
                            Hold the files
                        </Button>
                    </DialogFooter>
                </DialogContent>
            </Dialog>
        </>
    );
}

// The settings layout is applied by app.tsx for every page under settings/, so
// this supplies only the breadcrumb trail.
SecurityScan.layout = {
    breadcrumbs: [
        {
            title: 'Security scan',
            href: '/settings/security-scan',
        },
    ],
};
