/**
 * Git version control for Durable Objects using isomorphic-git
 */

import git from '@ashishkumar472/cf-git';
import { SqliteFS, type SqlExecutor } from './fs-adapter';
import type { FileOutputType } from '../schemas';
import * as Diff from 'diff';

export interface CommitInfo {
    oid: string;
    message: string;
    author: string;
    timestamp: string;
}

export interface FileDiff {
    path: string;
    diff: string;
}

export interface GitShowResult {
    oid: string;
    message: string;
    author: string;
    timestamp: string;
    files: number;
    fileList: string[];
    diffs?: FileDiff[];
}

type FileSnapshot = Omit<FileOutputType, 'filePurpose'>;

export class GitVersionControl {
    private onFilesChangedCallback?: () => void;
    public fs: SqliteFS;
    private author: { name: string; email: string };
    
    private get gitConfig() {
        return { fs: this.fs, dir: '/' } as const;
    }

    constructor(sql: SqlExecutor, author?: { name: string; email: string }) {
        this.fs = new SqliteFS(sql);
        this.author = author || { name: 'Vibesdk', email: 'vibesdk-bot@cloudflare.com' };
        
        // Initialize SQLite table synchronously
        this.fs.init();
    }

    setOnFilesChangedCallback(callback: () => void): void {
        this.onFilesChangedCallback = callback;
    }

    async getAllFilesFromHead(): Promise<Array<{ filePath: string; fileContents: string }>> {
        try {
            const oid = await git.resolveRef({ ...this.gitConfig, ref: 'HEAD' });
            return await this.readFilesFromCommit(oid);
        } catch {
            return [];
        }
    }

    async init(): Promise<void> {
        try {
            await git.init({ ...this.gitConfig, defaultBranch: 'main' });
            console.log('[Git] Repository initialized');
        } catch (error) {
            console.log('[Git] Repository already initialized:', error);
        }
    }

    /**
     * Stage files without committing them
     * Useful for batching multiple operations before a single commit
     */
    async stage(files: FileSnapshot[]): Promise<void> {
        if (!files.length) {
            console.log('[Git] No files to stage');
            return;
        }

        console.log(`[Git] Staging ${files.length} files`);

        for (const file of files) {
            try {
                const path = this.normalizePath(file.filePath);
                await this.fs.writeFile(path, file.fileContents);
                await git.add({ ...this.gitConfig, filepath: path, cache: {} });
            } catch (error) {
                console.error(`[Git] Failed to stage file ${file}:`, error);
                throw new Error(`Failed to stage file ${file}: ${error instanceof Error ? error.message : String(error)}`);
            }
        }

        console.log(`[Git] Staged ${files.length} files: ${files.map(f => f.filePath).join(', ')}`);
    }

    private normalizePath(path: string): string {
        return path.startsWith('/') ? path.slice(1) : path;
    }

    async commit(files: FileSnapshot[], message?: string): Promise<string | null> {
        console.log(`[Git] Starting commit with ${files.length} files`);
        if (files.length) {
            // Stage all files first
            await this.stage(files);
        } else if (!await this.hasChanges()) {
            // Only check for changes when no files were explicitly staged
            console.log('[Git] No changes to commit');
            return null;
        }

        console.log('[Git] Creating commit...');
        const oid = await git.commit({
            ...this.gitConfig,
            message: message || `Auto-checkpoint (${new Date().toISOString()})`,
            author: {
                name: this.author.name,
                email: this.author.email,
                timestamp: Math.floor(Date.now() / 1000)
            }
        });
        console.log(`[Git] Commit created: ${oid}`);
        return oid;
    }

    private async hasChanges(): Promise<boolean> {
        try {
            // Get top-level entries excluding .git to pass as filepaths.
            const entries = await this.fs.readdir('/');
            const workingTreePaths = entries.filter(e => e !== '.git');
            if (workingTreePaths.length === 0) return false;

            const status = await git.statusMatrix({
                ...this.gitConfig,
                filepaths: workingTreePaths,
            });
            return status.some(row => row[1] !== row[2]);
        } catch {
            return true;
        }
    }

    async log(limit = 50): Promise<CommitInfo[]> {
        try {
            const commits = await git.log({ ...this.gitConfig, depth: limit, ref: 'main' });
            return commits.map(c => ({
                oid: c.oid,
                message: c.commit.message,
                author: `${c.commit.author.name} <${c.commit.author.email}>`,
                timestamp: new Date(c.commit.author.timestamp * 1000).toISOString()
            }));
        } catch {
            return [];
        }
    }

    private async readFilesFromCommit(oid: string): Promise<FileSnapshot[]> {
        const { commit } = await git.readCommit({ ...this.gitConfig, oid });
        const files: FileSnapshot[] = [];
        await this.walkTree(commit.tree, '', files);
        return files;
    }

    async show(oid: string, options?: { includeDiff?: boolean }): Promise<GitShowResult> {
        const { commit } = await git.readCommit({ ...this.gitConfig, oid });
        
        if (!commit.parent || commit.parent.length === 0) {
            const files = await git.listFiles({ ...this.gitConfig, ref: oid });
            return this.formatShowResult(commit, oid, files);
        }
        
        const parentOid = commit.parent[0];
        const { commit: parentCommit } = await git.readCommit({ ...this.gitConfig, oid: parentOid });
        
        const [currentFileOids, parentFileOids] = await Promise.all([
            this.collectTreeOids(commit.tree),
            this.collectTreeOids(parentCommit.tree)
        ]);
        
        const changedFiles = this.findChangedFiles(currentFileOids, parentFileOids);
        const diffs = options?.includeDiff 
            ? await this.generateDiffs(changedFiles, currentFileOids, parentFileOids)
            : undefined;
        
        return this.formatShowResult(commit, oid, changedFiles, diffs);
    }

    private findChangedFiles(
        currentFileOids: Map<string, string>,
        parentFileOids: Map<string, string>
    ): string[] {
        const allPaths = new Set([...currentFileOids.keys(), ...parentFileOids.keys()]);
        const changedFiles: string[] = [];
        
        for (const path of allPaths) {
            if (currentFileOids.get(path) !== parentFileOids.get(path)) {
                changedFiles.push(path);
            }
        }
        
        return changedFiles;
    }

    private async generateDiffs(
        changedFiles: string[],
        currentFileOids: Map<string, string>,
        parentFileOids: Map<string, string>
    ): Promise<FileDiff[]> {
        const diffs: FileDiff[] = [];
        
        for (const path of changedFiles) {
            const [oldContent, newContent] = await Promise.all([
                this.readBlobContent(parentFileOids.get(path)),
                this.readBlobContent(currentFileOids.get(path))
            ]);
            
            if (oldContent !== newContent) {
                const diff = Diff.createPatch(path, oldContent, newContent);
                if (diff) {
                    diffs.push({ path, diff });
                }
            }
        }
        
        return diffs;
    }

    private async readBlobContent(oid: string | undefined): Promise<string> {
        if (!oid) return '';
        const { blob } = await git.readBlob({ ...this.gitConfig, oid });
        return new TextDecoder('utf-8').decode(blob);
    }

    private formatShowResult(
        commit: { message: string; author: { name: string; email: string; timestamp: number } },
        oid: string,
        fileList: string[],
        diffs?: FileDiff[]
    ): GitShowResult {
        return {
            oid,
            message: commit.message,
            author: `${commit.author.name} <${commit.author.email}>`,
            timestamp: new Date(commit.author.timestamp * 1000).toISOString(),
            files: fileList.length,
            fileList,
            ...(diffs && { diffs })
        };
    }

    private buildPath(prefix: string, name: string): string {
        return prefix ? `${prefix}/${name}` : name;
    }

    /**
     * Efficiently collect file paths and their OIDs from a tree (no blob content read)
     */
    private async collectTreeOids(treeOid: string, prefix: string = ''): Promise<Map<string, string>> {
        const fileOids = new Map<string, string>();
        const { tree } = await git.readTree({ ...this.gitConfig, oid: treeOid });
        
        for (const entry of tree) {
            const path = this.buildPath(prefix, entry.path);
            
            if (entry.type === 'blob') {
                fileOids.set(path, entry.oid);
            } else if (entry.type === 'tree') {
                const subtreeOids = await this.collectTreeOids(entry.oid, path);
                for (const [subpath, oid] of subtreeOids) {
                    fileOids.set(subpath, oid);
                }
            }
        }
        
        return fileOids;
    }

    async reset(ref: string, options?: { hard?: boolean }): Promise<{ ref: string; filesReset: number }> {
        const oid = await git.resolveRef({ ...this.gitConfig, ref });
        await git.writeRef({ ...this.gitConfig, ref: 'HEAD', value: oid, force: true });
        
        if (options?.hard !== false) {
            await git.checkout({ ...this.gitConfig, ref, force: true });
        }
        
        const files = await git.listFiles({ ...this.gitConfig, ref });
        
        this.onFilesChangedCallback?.();
        
        return { ref, filesReset: files.length };
    }

    private async walkTree(treeOid: string, prefix: string, files: FileSnapshot[]): Promise<void> {
        const { tree } = await git.readTree({ ...this.gitConfig, oid: treeOid });

        for (const entry of tree) {
            const path = this.buildPath(prefix, entry.path);

            if (entry.type === 'blob') {
                const textContent = await this.tryReadTextBlob(entry.oid);
                if (textContent) {
                    files.push({ filePath: path, fileContents: textContent });
                }
            } else if (entry.type === 'tree') {
                await this.walkTree(entry.oid, path, files);
            }
        }
    }

    private async tryReadTextBlob(oid: string): Promise<string | null> {
        try {
            const { blob } = await git.readBlob({ ...this.gitConfig, oid });
            const content = new TextDecoder('utf-8').decode(blob);
            return content.includes('\0') ? null : content;
        } catch {
            return null;
        }
    }

    async getHead(): Promise<string | null> {
        try {
            let timeoutId: ReturnType<typeof setTimeout> | null = null;
            
            const timeoutPromise = new Promise<never>((_, reject) => {
                timeoutId = setTimeout(() => {
                    reject(new Error('git.resolveRef timed out after 5 seconds'));
                }, 5000);
            });
            
            const resolvePromise = git.resolveRef({ ...this.gitConfig, ref: 'HEAD' });
            
            try {
                const result = await Promise.race([resolvePromise, timeoutPromise]);
                return result;
            } finally {
                if (timeoutId !== null) {
                    clearTimeout(timeoutId);
                }
            }
        } catch {
            return null;
        }
    }

    /**
     * Get storage statistics for monitoring and observability
     */
    getStorageStats(): { totalObjects: number; totalBytes: number; largestObject: { path: string; size: number } | null } {
        return this.fs.getStorageStats();
    }
}