import React from 'react';
import { Form, Formik, useFormikContext } from 'formik';
import { object, string } from 'yup';
import { ServerContext } from '@/state/server';
import { pullFromUrl, queryPullUrl, getPullUrlStatus, cancelPullUrl, PullUrlQueryResponse } from '../api/server/files/betterFilesApi';
import { getDirectoryContents, type FileObject } from '../api/server/files';
import { useFlashKey } from '../useFlash';
import { Dialog } from '@/components/elements/dialog';
import { Button } from '@/components/elements/button/index';
import Field from '@/components/elements/Field';
import Code from '@/components/elements/Code';
import GreyRowBox from '@/components/elements/GreyRowBox';

interface Props {
    visible: boolean;
    onDismiss: () => void;
    directories: string[];
    onPullComplete: () => Promise<void>;
}

interface PullUrlFormValues {
    url: string;
    filename: string;
    directory: string;
}

interface ActivePull {
    id: string;
    directory: string;
    progress: number;
    status: 'running' | 'done';
    misses: number;
    expectedName: string;
    expectedSize: number;
    startedAt: number;
    seen: boolean;
}

const formatBytes = (bytes: number): string => {
    if (!bytes || bytes <= 0) return 'Unknown';
    const units = ['B', 'KB', 'MB', 'GB', 'TB'];
    let value = bytes;
    let unitIndex = 0;
    while (value >= 1024 && unitIndex < units.length - 1) {
        value /= 1024;
        unitIndex += 1;
    }
    return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`;
};

const getErrorMessage = (error: any): string => {
    const response = error?.response?.data;
    if (response?.errors && Array.isArray(response.errors) && response.errors[0]?.detail) {
        return response.errors[0].detail;
    }
    if (response?.error) return response.error;
    if (response?.message) return response.message;
    return error?.message || 'Failed to query URL.';
};

const getFileExtension = (name: string): string => {
    const lastDot = name.lastIndexOf('.');
    if (lastDot <= 0 || lastDot === name.length - 1) return '';
    return name.slice(lastDot + 1);
};

const buildExpectedName = (inputName: string, queriedName: string | null | undefined): string => {
    const cleanedInput = inputName?.trim() || '';
    if (!cleanedInput) return queriedName || 'filename_from_url';
    const inputExt = getFileExtension(cleanedInput);
    if (inputExt) return cleanedInput;
    const queryExt = queriedName ? getFileExtension(queriedName) : '';
    return queryExt ? `${cleanedInput}.${queryExt}` : cleanedInput;
};

const normalizeDirectory = (path: string): string => {
    const normalized = (path || '/').trim().replace(/\\/g, '/').replace(/\/+/g, '/');
    if (!normalized || normalized === '.') return '/';
    const withSlash = normalized.startsWith('/') ? normalized : `/${normalized}`;
    return withSlash.length > 1 ? withSlash.replace(/\/+$/, '') : '/';
};

const PullUrlFormBody: React.FC<{
    uuid: string;
    targetDirectories: string[];
    onDismiss: () => void;
    onPullComplete: () => Promise<void>;
    onError: (error: Error) => void;
}> = ({ uuid, targetDirectories, onDismiss, onPullComplete, onError }) => {
    const { values, validateForm, setSubmitting, setTouched, isSubmitting } = useFormikContext<PullUrlFormValues>();
    const [queryData, setQueryData] = React.useState<PullUrlQueryResponse | null>(null);
    const [queryError, setQueryError] = React.useState<string | null>(null);
    const [queryLoading, setQueryLoading] = React.useState(false);
    const queryRequestRef = React.useRef(0);
    const [activePulls, setActivePulls] = React.useState<ActivePull[]>([]);
    const activePullsRef = React.useRef<ActivePull[]>([]);
    const pollingRef = React.useRef<number | null>(null);
    const completionNotifiedRef = React.useRef(false);

    React.useEffect(() => {
        activePullsRef.current = activePulls;
        if (activePulls.length > 0) {
            completionNotifiedRef.current = false;
        }
    }, [activePulls]);

    React.useEffect(() => {
        const url = values.url.trim();
        if (!url) {
            setQueryData(null);
            setQueryError(null);
            setQueryLoading(false);
            return;
        }
        if (!/^https?:\/\//i.test(url)) {
            setQueryData(null);
            setQueryError(null);
            setQueryLoading(false);
            return;
        }

        const timeout = window.setTimeout(async () => {
            const requestId = queryRequestRef.current + 1;
            queryRequestRef.current = requestId;
            setQueryLoading(true);
            setQueryError(null);
            try {
                const data = await queryPullUrl(uuid, url);
                if (queryRequestRef.current === requestId) {
                    setQueryData(data);
                }
            } catch (error) {
                if (queryRequestRef.current === requestId) {
                    setQueryData(null);
                    setQueryError(getErrorMessage(error));
                }
            } finally {
                if (queryRequestRef.current === requestId) {
                    setQueryLoading(false);
                }
            }
        }, 450);

        return () => window.clearTimeout(timeout);
    }, [uuid, values.url]);

    React.useEffect(() => {
        if (activePullsRef.current.length === 0) {
            if (pollingRef.current) {
                window.clearInterval(pollingRef.current);
                pollingRef.current = null;
            }
            return;
        }

        if (pollingRef.current) return;

        pollingRef.current = window.setInterval(async () => {
            try {
                const status = await getPullUrlStatus(uuid);
                const downloads = Array.isArray(status.downloads) ? status.downloads : [];

                const next: ActivePull[] = activePullsRef.current.map((pull): ActivePull => {
                    const match = downloads.find((dl) => dl.identifier === pull.id);
                    if (!match) {
                        return {
                            ...pull,
                            misses: pull.misses + 1,
                        };
                    }
                    const progress = Math.min(100, Math.max(0, Math.round(match.progress * 100)));
                    return {
                        ...pull,
                        progress,
                        status: (progress >= 100 ? 'done' : 'running'),
                        misses: 0,
                    };
                });

                const missing = next.filter((pull) => pull.status === 'running' && pull.misses >= 1);
                if (missing.length > 0) {
                    const directories = Array.from(new Set(missing.map((pull) => pull.directory)));
                    const directoryResults = await Promise.all(
                        directories.map((directory) =>
                            getDirectoryContents(uuid, directory)
                                .then((value) => ({ status: 'fulfilled' as const, value }))
                                .catch((error) => ({ status: 'rejected' as const, reason: error }))
                        )
                    );

                    const directoryMap = new Map<string, FileObject[]>(
                        directoryResults.map((result, index) => {
                            const entries = result.status === 'fulfilled' ? result.value : [];
                            return [directories[index], entries];
                        })
                    );

                    for (const pull of next) {
                        if (pull.status !== 'running' || pull.expectedSize <= 0) continue;
                        const entries = directoryMap.get(pull.directory);
                        if (!entries || entries.length === 0) continue;
                        const fileEntry = entries.find((entry) => entry.name === pull.expectedName);
                        if (!fileEntry) continue;
                        const progress = Math.min(100, Math.max(0, Math.round((fileEntry.size / pull.expectedSize) * 100)));
                        pull.progress = Math.max(pull.progress, progress);
                        pull.misses = 0;
                        pull.seen = true;
                        if (fileEntry.size >= pull.expectedSize) {
                            pull.status = 'done';
                        }
                    }

                    for (const pull of next) {
                        if (pull.status !== 'running' || pull.expectedSize > 0) continue;
                        const entries = directoryMap.get(pull.directory);
                        if (!entries || entries.length === 0) continue;
                        const fileEntry = entries.find((entry) => entry.name === pull.expectedName);
                        if (!fileEntry) continue;
                        pull.progress = 100;
                        pull.status = 'done';
                        pull.misses = 0;
                        pull.seen = true;
                    }
                }

                if (downloads.length === 0) {
                    const now = Date.now();
                    for (const pull of next) {
                        if (pull.status !== 'running') continue;
                        if (pull.misses < 2) continue;
                        if (now - pull.startedAt < 1500) continue;
                        if (!pull.seen && pull.expectedSize > 0) continue;
                        pull.progress = 100;
                        pull.status = 'done';
                        pull.misses = 0;
                    }
                }

                setActivePulls([...next]);

                const remaining = next.filter((pull) => pull.status === 'running');
                if (remaining.length === 0 && !completionNotifiedRef.current) {
                    completionNotifiedRef.current = true;
                    await onPullComplete();
                    if (pollingRef.current) {
                        window.clearInterval(pollingRef.current);
                        pollingRef.current = null;
                    }
                }
            } catch (error) {

            }
        }, 1200);

        return () => {
            if (pollingRef.current) {
                window.clearInterval(pollingRef.current);
                pollingRef.current = null;
            }
        };
    }, [uuid, onPullComplete, activePulls.length]);

    const resolvedName = buildExpectedName(values.filename, queryData?.filename);
    const sizeLabel = queryData?.size ? formatBytes(queryData.size) : 'Unknown';

    const handleCancelPull = async (id: string) => {
        try {
            await cancelPullUrl(uuid, id);
            setActivePulls((prev) => prev.filter((pull) => pull.id !== id));
        } catch (error) {
            onError(error as Error);
        }
    };

    const handlePull = async () => {
        const errors = await validateForm();
        if (Object.keys(errors).length > 0) {
            setTouched({ url: true, filename: true }, true);
            return;
        }
        setSubmitting(true);
        try {
            const expectedName = buildExpectedName(values.filename, queryData?.filename);
            const expectedSize = queryData?.size ? Number(queryData.size) : 0;

            const directories = values.directory.trim()
                ? [normalizeDirectory(values.directory)]
                : targetDirectories;

            for (const directory of directories) {
                const response = await pullFromUrl(uuid, {
                    url: values.url.trim(),
                    directory,
                    filename: values.filename || undefined,
                });
                const id = response.identifier || response.id;
                if (!id) {
                    throw new Error('Failed to start pull. No identifier returned.');
                }
                const resolvedExpectedName = response.filename || expectedName;
                const resolvedExpectedSize = typeof response.size === 'number' ? response.size : expectedSize;
                const resolvedDirectory = response.directory || directory;
                setActivePulls((prev) => [
                    ...prev,
                    {
                        id,
                        directory: resolvedDirectory,
                        progress: 0,
                        status: 'running',
                        misses: 0,
                        expectedName: resolvedExpectedName,
                        expectedSize: resolvedExpectedSize,
                        startedAt: Date.now(),
                        seen: false,
                    },
                ]);
            }
        } catch (error) {
            onError(error as Error);
        } finally {
            setSubmitting(false);
        }
    };

    const selectedTargetDirectories = values.directory.trim()
        ? [normalizeDirectory(values.directory)]
        : targetDirectories;
    const previewDirectory = selectedTargetDirectories[0] || '/';
    const previewPath = `${previewDirectory === '/' ? '' : previewDirectory}/${resolvedName}`;

    return (
        <Form onSubmit={(event) => {
            event.preventDefault();
            void handlePull();
        }}>
            <Field
                autoFocus
                id="url"
                name="url"
                label="File URL"
                description="Enter the URL of the file you want to download"
            />
            <Field
                id="filename"
                name="filename"
                label="Filename (optional)"
                description="Leave empty to use the filename from the URL"
            />
            <Field
                id="directory"
                name="directory"
                label="Destination directory (optional)"
                description="Leave empty to use the selected folder, or enter a specific path like /plugins."
            />
            <GreyRowBox
                className="mt-3 px-3 py-2 text-sm text-neutral-300 flex flex-col items-start"
                $hoverable={false}
            >
                <div className="flex items-center justify-between w-full">
                    <span className="text-xs text-gray-500">Detected file</span>
                    {queryLoading ? <span className="text-xs text-gray-500">Checking...</span> : null}
                </div>
                <div className="mt-2 flex flex-wrap items-center gap-2 w-full">
                    <Code>{queryData?.filename || resolvedName}</Code>
                    <span className="text-xs text-gray-500">Size: {sizeLabel}</span>
                </div>
                {queryError && (
                    <p className="mt-2 text-xs text-red-400">{queryError}</p>
                )}
            </GreyRowBox>

            {selectedTargetDirectories.length === 1 ? (
                <p className="mt-2 text-sm text-neutral-300">
                    Will be saved to: <Code>{previewPath}</Code>
                </p>
            ) : (
                <p className="mt-2 text-sm text-neutral-300">
                    Will be saved to <Code>{targetDirectories.length}</Code> directories.
                </p>
            )}

            {activePulls.length > 0 && (
                <div className="mt-4 space-y-2">
                    {activePulls.map((pull) => (
                        <GreyRowBox
                            key={pull.id}
                            className="px-3 py-2 text-sm text-neutral-300"
                            $hoverable={false}
                        >
                            <div className="flex items-center justify-between gap-3">
                                <div className="min-w-0">
                                    <div className="truncate">Pulling to <Code>{pull.directory}</Code></div>
                                    <div className="text-xs text-gray-500">ID: {pull.id.slice(0, 8)}...</div>
                                </div>
                                <div className="flex items-center gap-2">
                                    <span className="text-xs text-gray-500">
                                        {pull.status === 'done'
                                            ? 'Completed'
                                            : pull.misses >= 5
                                                ? 'Waiting for status'
                                                : `${pull.progress}%`}
                                    </span>
                                    {pull.status === 'running' && (
                                        <Button.Danger
                                            type="button"
                                            size={Button.Sizes.Small}
                                            onClick={() => handleCancelPull(pull.id)}
                                        >
                                            Cancel
                                        </Button.Danger>
                                    )}
                                </div>
                            </div>
                            <div
                                className="mt-2 h-1.5 w-full overflow-hidden bg-gray-600"
                                
                            >
                                <div
                                    className="h-full bg-blue-500 transition-[width] duration-200"
                                    style={{ width: `${pull.progress}%` }}
                                />
                            </div>
                        </GreyRowBox>
                    ))}
                </div>
            )}

            <Dialog.Footer>
                <Button.Text onClick={onDismiss}>
                    Close
                </Button.Text>
                <Button onClick={() => void handlePull()} disabled={isSubmitting}>
                    Pull File
                </Button>
            </Dialog.Footer>
        </Form>
    );
};

export const PullUrlInlineForm: React.FC<{
    directories: string[];
    onDismiss: () => void;
    onPullComplete: () => Promise<void>;
}> = ({ directories, onDismiss, onPullComplete }) => {
    const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
    const { clearAndAddHttpError } = useFlashKey('better-files');
    const targetDirectories = Array.from(
        new Set(
            (directories.length ? directories : ['/']).map((path) => (path.startsWith('/') ? path : `/${path}`))
        )
    );

    return (
        <div className="betterfiles-scope" onMouseDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()}>
            <Formik
                initialValues={{ url: '', filename: '', directory: targetDirectories.length === 1 ? targetDirectories[0] : '' }}
                validationSchema={object().shape({
                    url: string().url('Must be a valid URL').required('URL is required'),
                    filename: string(),
                    directory: string(),
                })}
                onSubmit={async (_values, { setSubmitting }) => {
                    setSubmitting(false);
                }}
            >
                <PullUrlFormBody
                    uuid={uuid}
                    targetDirectories={targetDirectories}
                    onDismiss={onDismiss}
                    onPullComplete={onPullComplete}
                    onError={clearAndAddHttpError}
                />
            </Formik>
        </div>
    );
};

const PullUrlModal: React.FC<Props> = ({ visible, onDismiss, directories, onPullComplete }) => {
    return (
        <Dialog
            open={visible}
            onClose={onDismiss}
            title="Pull File from URL"
        >
            <PullUrlInlineForm
                directories={directories}
                onDismiss={onDismiss}
                onPullComplete={onPullComplete}
            />
        </Dialog>
    );
};

export default PullUrlModal;
