import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { httpErrorToHuman } from '@/api/http';
import { Dialog } from '@/components/elements/dialog';
import { Button } from '@/components/elements/button/index';
import GreyRowBox from '@/components/elements/GreyRowBox';
import Code from '@/components/elements/Code';
import Input from '@/components/elements/Input';
import Label from '@/components/elements/Label';
import Spinner from '@/components/elements/Spinner';
import {
    cloneGitRepository,
    getGitDiff,
    getGitStatus,
    pullGitRepository,
    type GitOperationResult,
    type GitStatus,
} from '../api/server/files/git';

const cx = (...parts: Array<string | false | null | undefined>) => parts.filter(Boolean).join(' ');

type ResultState = { type: 'success' | 'error'; text: string };

interface GitPanelProps {
    open: boolean;
    onClose: () => void;
    uuid: string;
    workDir?: string;
    onFilesChanged?: () => void;
}

const SectionLabel = ({ children, className }: { children: React.ReactNode; className?: string }) => (
    <p className={cx('text-xs font-medium text-neutral-400 mb-2', className)}>{children}</p>
);

const Notice = ({ title, children }: { title: string; children: React.ReactNode }) => (
    <GreyRowBox $hoverable={false} className={'flex-col !items-stretch'}>
        <p className={'text-sm font-semibold text-neutral-100'}>{title}</p>
        <p className={'mt-1 text-sm text-neutral-400'}>{children}</p>
    </GreyRowBox>
);

const OperationResult = ({ result }: { result: ResultState | null }) => {
    if (!result) return null;

    return (
        <GreyRowBox
            $hoverable={false}
            className={cx(
                'mt-4 flex-col !items-stretch !p-3',
                result.type === 'success'
                    ? '!border-green-500/30 bg-green-500/10 text-green-300'
                    : '!border-red-500/30 bg-red-500/10 text-red-300'
            )}
        >
            <pre className={'m-0 max-h-32 overflow-auto whitespace-pre-wrap text-xs font-mono'}>{result.text}</pre>
        </GreyRowBox>
    );
};

const statusLineClass = (code: string) =>
    cx(
        'min-w-0 flex-1 truncate text-xs font-mono',
        code.includes('M') && 'text-yellow-300',
        code.includes('A') && 'text-green-300',
        code.includes('D') && 'text-red-300',
        code.includes('?') && 'text-neutral-400',
        !code.match(/[MAD?]/) && 'text-neutral-300'
    );

type DiffLineKind =
    | 'section'
    | 'file'
    | 'old-file'
    | 'new-file'
    | 'hunk'
    | 'added'
    | 'removed'
    | 'meta'
    | 'context';

const getDiffLineKind = (line: string): DiffLineKind => {
    if (line === 'Staged changes:' || line === 'Unstaged changes:') return 'section';
    if (line.startsWith('diff --git ')) return 'file';
    if (line.startsWith('--- ')) return 'old-file';
    if (line.startsWith('+++ ')) return 'new-file';
    if (line.startsWith('@@')) return 'hunk';
    if (line.startsWith('+')) return 'added';
    if (line.startsWith('-')) return 'removed';
    if (/^(index |new file mode|deleted file mode|similarity index|rename from|rename to)/.test(line)) return 'meta';

    return 'context';
};

const formatDiffFilePath = (line: string) => {
    const match = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
    return match ? match[2] : line.replace(/^diff --git\s+/, '');
};

const formatFileMarkerPath = (line: string) => {
    const marker = line.replace(/^(---|\+\+\+)\s+/, '');
    return marker === '/dev/null' ? marker : marker.replace(/^[ab]\//, '');
};

const formatHunkLabel = (line: string) => {
    const match = line.match(/^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@\s*(.*)$/);
    if (!match) return line;

    const scope = match[3] ? ` ${match[3]}` : '';
    return `Lines ${match[1]} -> ${match[2]}${scope}`;
};

const DiffOutputLine = ({ line }: { line: string }) => {
    const kind = getDiffLineKind(line);

    if (kind === 'section') {
        return (
            <div
                className={
                    'mt-2 first:mt-0 rounded bg-neutral-700 px-3 py-2 text-xs font-semibold text-neutral-300'
                }
            >
                {line}
            </div>
        );
    }

    if (kind === 'file') {
        return (
            <div
                className={
                    'mt-3 first:mt-0 flex min-w-max items-center gap-2 rounded-t border border-neutral-600 bg-neutral-700 px-3 py-2 text-neutral-100'
                }
            >
                <span className={'rounded bg-neutral-600 px-1.5 py-0.5 text-xs text-neutral-300'}>
                    File
                </span>
                <span className={'font-semibold'}>{formatDiffFilePath(line)}</span>
            </div>
        );
    }

    if (kind === 'old-file' || kind === 'new-file') {
        const isOld = kind === 'old-file';

        return (
            <div
                className={cx(
                    'flex min-w-max items-center gap-2 border-l-2 px-3 py-1 text-[11px]',
                    isOld ? 'border-red-500/50 bg-red-500/10 text-red-200' : 'border-green-500/50 bg-green-500/10 text-green-200'
                )}
            >
                <span className={'w-12 shrink-0 text-xs text-neutral-400'}>
                    {isOld ? 'Before' : 'After'}
                </span>
                <span>{formatFileMarkerPath(line)}</span>
            </div>
        );
    }

    if (kind === 'hunk') {
        return (
            <div
                className={
                    'mt-2 flex min-w-max items-center gap-2 border-l-2 border-blue-500 bg-blue-500/10 px-3 py-1.5 text-blue-200'
                }
            >
                <span className={'text-xs text-blue-300'}>Hunk</span>
                <span>{formatHunkLabel(line)}</span>
                <span className={'text-neutral-500'}>{line}</span>
            </div>
        );
    }

    const symbol = kind === 'added' ? '+' : kind === 'removed' ? '-' : line.startsWith(' ') ? ' ' : '';
    const content = kind === 'added' || kind === 'removed' || line.startsWith(' ') ? line.substring(1) : line;

    return (
        <div
            className={cx(
                'flex min-w-max items-start border-l-2 px-2 py-0.5 leading-5',
                kind === 'added' && 'border-green-500 bg-green-500/10 text-green-200',
                kind === 'removed' && 'border-red-500 bg-red-500/10 text-red-200',
                kind === 'meta' && 'border-neutral-700 text-neutral-500',
                kind === 'context' && 'border-transparent text-neutral-300'
            )}
        >
            <span className={'w-5 shrink-0 select-none text-center text-[10px] font-bold'}>{symbol}</span>
            <span className={'pl-2 whitespace-pre'}>{content || ' '}</span>
        </div>
    );
};

const GitPanel: React.FC<GitPanelProps> = ({ open, uuid, workDir, onClose, onFilesChanged }) => {
    const [loading, setLoading] = useState(true);
    const [status, setStatus] = useState<GitStatus | null>(null);
    const [activeTab, setActiveTab] = useState<'status' | 'diff' | 'log'>('status');
    const [operating, setOperating] = useState(false);
    const [result, setResult] = useState<ResultState | null>(null);
    const [repositoryUrl, setRepositoryUrl] = useState('');
    const [targetDirectory, setTargetDirectory] = useState('');
    const [diffOutput, setDiffOutput] = useState('');
    const [diffLoading, setDiffLoading] = useState(false);

    const currentWorkDir = workDir || '/';
    const wingsAvailable = status?.error !== 'Failed to connect';
    const gitMissing = !!status && wingsAvailable && !status.available;

    const statusLines = useMemo(() => status?.status?.split('\n').filter(Boolean) || [], [status?.status]);
    const changedLines = useMemo(() => statusLines.filter((line) => !line.startsWith('##')), [statusLines]);
    const branch = status?.branch || 'unknown';

    const loadDiff = useCallback(async () => {
        setDiffLoading(true);

        try {
            const response = await getGitDiff(uuid, currentWorkDir);
            setDiffOutput(response.stdout || response.stderr || '(no tracked diff)');
        } catch (e: any) {
            setDiffOutput(httpErrorToHuman(e) || 'Failed to load diff.');
        } finally {
            setDiffLoading(false);
        }
    }, [uuid, currentWorkDir]);

    const refresh = useCallback(
        async (clearResult = true) => {
            setLoading(true);
            if (clearResult) setResult(null);

            try {
                setStatus(await getGitStatus(uuid, currentWorkDir));
            } catch {
                setStatus({ available: false, is_repo: false, error: 'Failed to connect' });
            } finally {
                setLoading(false);
            }
        },
        [uuid, currentWorkDir]
    );

    useEffect(() => {
        if (open) refresh();
    }, [open, refresh]);

    useEffect(() => {
        if (open && activeTab === 'diff' && status?.is_repo) {
            loadDiff();
        }
    }, [open, activeTab, status?.is_repo, loadDiff]);

    const runOperation = async (
        label: string,
        action: () => Promise<GitOperationResult>
    ): Promise<GitOperationResult | null> => {
        setOperating(true);
        setResult(null);

        try {
            const response = await action();
            const text = response.stderr || response.stdout || `${label} completed.`;

            setResult({
                type: response.exit_code === 0 ? 'success' : 'error',
                text,
            });

            await refresh(false);
            return response;
        } catch (e: any) {
            setResult({ type: 'error', text: httpErrorToHuman(e) || `${label} failed.` });
            return null;
        } finally {
            setOperating(false);
        }
    };

    const handleClone = async () => {
        const cleanedUrl = repositoryUrl.trim();
        if (!cleanedUrl) return;

        const response = await runOperation('Clone', () =>
            cloneGitRepository(uuid, cleanedUrl, targetDirectory.trim(), currentWorkDir)
        );

        if (response?.exit_code === 0) {
            setRepositoryUrl('');
            setTargetDirectory('');
            onFilesChanged?.();
        }
    };

    const handlePull = async () => {
        const response = await runOperation('Pull', () => pullGitRepository(uuid, currentWorkDir));
        if (response?.exit_code === 0) {
            onFilesChanged?.();
            if (activeTab === 'diff') {
                await loadDiff();
            }
        }
    };

    return (
        <Dialog open={open} onClose={onClose} title={'Git'}>
            {loading ? (
                <div className={'py-12 flex justify-center'}>
                    <Spinner size={'large'} />
                </div>
            ) : !wingsAvailable ? (
                <Notice title={'Wings Git Mod Not Installed'}>
                    The daemon on this node does not expose the safe Git endpoints.
                </Notice>
            ) : gitMissing ? (
                <Notice title={'Git Not Installed'}>
                    Install Git in the server image or use an image that already includes Git.
                </Notice>
            ) : !status?.is_repo ? (
                <div className={'space-y-4'}>
                    <GreyRowBox $hoverable={false} className={'flex-col !items-stretch'}>
                        <SectionLabel>Current Folder</SectionLabel>
                        <Code dark className={'max-w-full truncate'}>
                            {currentWorkDir}
                        </Code>
                    </GreyRowBox>

                    <Notice title={'No Repository Here'}>
                        Clone a public HTTPS repository into this folder. Leave the target directory blank to clone into
                        the current folder.
                    </Notice>

                    <GreyRowBox $hoverable={false} className={'flex-col !items-stretch'}>
                        <div className={'space-y-4'}>
                            <div>
                                <Label htmlFor={'git_repository_url'}>Repository URL</Label>
                                <Input
                                    id={'git_repository_url'}
                                    type={'text'}
                                    placeholder={'https://github.com/user/repo.git'}
                                    value={repositoryUrl}
                                    onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                                        setRepositoryUrl(e.target.value)
                                    }
                                />
                                <p className={'input-help'}>Only public HTTPS remotes are accepted by Wings.</p>
                            </div>
                            <div>
                                <Label htmlFor={'git_target_directory'}>Target Directory</Label>
                                <Input
                                    id={'git_target_directory'}
                                    type={'text'}
                                    placeholder={'Optional; blank = current folder'}
                                    value={targetDirectory}
                                    onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                                        setTargetDirectory(e.target.value)
                                    }
                                />
                                <p className={'input-help'}>
                                    The target must be empty. The Better Files trash folder is ignored automatically.
                                </p>
                            </div>
                            <div className={'flex justify-end'}>
                                <Button
                                    type={'button'}
                                    size={Button.Sizes.Small}
                                    onClick={handleClone}
                                    disabled={operating || !repositoryUrl.trim()}
                                >
                                    {operating ? 'Cloning...' : 'Clone'}
                                </Button>
                            </div>
                        </div>
                    </GreyRowBox>

                    <OperationResult result={result} />
                </div>
            ) : (
                <div className={'space-y-4'}>
                    <GreyRowBox
                        $hoverable={false}
                        className={'flex-col gap-4 !items-stretch sm:flex-row sm:!items-center sm:justify-between'}
                    >
                        <div className={'min-w-0'}>
                            <SectionLabel>Repository</SectionLabel>
                            <div className={'flex items-center gap-2 flex-wrap'}>
                                <Code dark>{branch}</Code>
                                {changedLines.length > 0 && (
                                    <span className={'text-xs text-yellow-400'}>{changedLines.length} changed</span>
                                )}
                            </div>
                            <p className={'mt-2 truncate text-xs font-mono text-neutral-500'}>{currentWorkDir}</p>
                        </div>

                        <div className={'flex gap-2 justify-end shrink-0'}>
                            <Button.Text
                                type={'button'}
                                size={Button.Sizes.Small}
                                onClick={() => refresh()}
                                disabled={operating}
                            >
                                Refresh
                            </Button.Text>
                            <Button type={'button'} size={Button.Sizes.Small} onClick={handlePull} disabled={operating}>
                                {operating ? 'Pulling...' : 'Pull'}
                            </Button>
                        </div>
                    </GreyRowBox>

                    <GreyRowBox $hoverable={false} className={'!p-2 flex-wrap gap-2'}>
                        {(['status', 'diff', 'log'] as const).map((tab) => (
                            <Button.Text
                                key={tab}
                                type={'button'}
                                size={Button.Sizes.Small}
                                onClick={() => setActiveTab(tab)}
                                className={cx('capitalize', activeTab === tab && 'bg-neutral-600 text-neutral-100')}
                            >
                                {tab}
                            </Button.Text>
                        ))}
                    </GreyRowBox>

                    {activeTab === 'status' && (
                        <div className={'space-y-3'}>
                            {changedLines.length === 0 ? (
                                <GreyRowBox $hoverable={false}>
                                    <p className={'text-sm text-neutral-400'}>Working tree clean</p>
                                </GreyRowBox>
                            ) : (
                                <div className={'space-y-2'}>
                                    {changedLines.map((line, index) => {
                                        const code = line.substring(0, 2);
                                        const file = line.substring(3);

                                        return (
                                            <GreyRowBox
                                                key={`${line}-${index}`}
                                                $hoverable={false}
                                                className={'!p-3 gap-3'}
                                            >
                                                <Code dark className={'w-10 shrink-0 text-center text-xs'}>
                                                    {code}
                                                </Code>
                                                <p className={statusLineClass(code)}>{file}</p>
                                            </GreyRowBox>
                                        );
                                    })}
                                </div>
                            )}

                            {status?.remotes && (
                                <GreyRowBox $hoverable={false} className={'flex-col !items-stretch'}>
                                    <SectionLabel>Remotes</SectionLabel>
                                    <pre
                                        className={
                                            'm-0 max-h-24 overflow-auto rounded bg-neutral-800 p-3 text-xs text-neutral-400 font-mono whitespace-pre-wrap'
                                        }
                                    >
                                        {status.remotes}
                                    </pre>
                                </GreyRowBox>
                            )}
                        </div>
                    )}

                    {activeTab === 'diff' && (
                        <GreyRowBox $hoverable={false} className={'flex-col !items-stretch !p-0'}>
                            <div className={'flex items-center justify-between gap-3 border-b border-neutral-600 px-4 py-3'}>
                                <SectionLabel className={'mb-0'}>Actual Changes</SectionLabel>
                                <Button.Text
                                    type={'button'}
                                    size={Button.Sizes.Small}
                                    onClick={loadDiff}
                                    disabled={diffLoading}
                                >
                                    Refresh
                                </Button.Text>
                            </div>
                            <div className={'max-h-80 overflow-auto bg-neutral-800 p-2 text-xs font-mono'}>
                                {diffLoading ? (
                                    <div className={'p-2 text-neutral-400'}>Loading diff...</div>
                                ) : (
                                    (diffOutput || '(no tracked diff)').split('\n').map((line, index) => (
                                        <DiffOutputLine key={`${index}-${line}`} line={line} />
                                    ))
                                )}
                            </div>
                        </GreyRowBox>
                    )}

                    {activeTab === 'log' && (
                        <GreyRowBox $hoverable={false} className={'flex-col !items-stretch'}>
                            <SectionLabel>Recent Commits</SectionLabel>
                            <pre
                                className={
                                    'm-0 max-h-64 overflow-auto rounded bg-neutral-800 p-3 text-xs text-neutral-300 font-mono whitespace-pre-wrap'
                                }
                            >
                                {status?.log || '(no commits)'}
                            </pre>
                        </GreyRowBox>
                    )}

                    <OperationResult result={result} />
                </div>
            )}

            <Dialog.Footer>
                <Button.Text onClick={onClose}>Close</Button.Text>
            </Dialog.Footer>
        </Dialog>
    );
};

export default GitPanel;
