/**
 * GitHub service for repository creation and export
 */

import { Octokit } from '@octokit/rest';
import { createLogger } from '../../logger';
import {
    GitHubRepository,
    CreateRepositoryOptions,
    CreateRepositoryResult,
    GitHubServiceError,
} from './types';
import { GitHubPushResponse, TemplateDetails } from '../sandbox/sandboxTypes';
import { GitCloneService } from '../../agents/git/git-clone-service';
import git from '@ashishkumar472/cf-git';
import http from '@ashishkumar472/cf-git/http/web';
import { prepareCloudflareButton } from '../../utils/deployToCf';
import type { MemFS } from '../../agents/git/memfs';

export class GitHubService {
    private static readonly logger = createLogger('GitHubService');

    static createOctokit(token: string): Octokit {
        if (!token?.trim()) {
            throw new GitHubServiceError('No GitHub token provided', 'NO_TOKEN');
        }
        return new Octokit({ auth: token });
    }
    
    /**
     * Create a new GitHub repository
     */
    static async createUserRepository(
        options: CreateRepositoryOptions
    ): Promise<CreateRepositoryResult> {
        const autoInit = options.auto_init ?? true;
        
        GitHubService.logger.info('Creating GitHub repository', {
            name: options.name,
            private: options.private,
            auto_init: autoInit,
            description: options.description ? 'provided' : 'none'
        });
        
        try {
            const octokit = GitHubService.createOctokit(options.token);
            
            const { data: repository } = await octokit.repos.createForAuthenticatedUser({
                name: options.name,
                description: options.description,
                private: options.private,
                auto_init: autoInit,
            });

            GitHubService.logger.info('Successfully created repository', {
                html_url: repository.html_url
            });

            return {
                success: true,
                repository: repository as GitHubRepository
            };
        } catch (error: unknown) {
            const octokitError = error as { status?: number; message?: string; response?: { data?: { errors?: Array<{ field?: string; message?: string }> } } };
            
            GitHubService.logger.error('Repository creation failed', {
                status: octokitError?.status,
                message: octokitError?.message,
                name: options.name
            });
            
            if (octokitError?.status === 403) {
                return {
                    success: false,
                    error: 'GitHub App lacks required permissions. Please ensure the app has Contents: Write and Metadata: Read permissions, then re-install it.'
                };
            }
            
            // Check if repository already exists (422 Unprocessable Entity)
            if (octokitError?.status === 422) {
                const hasNameExistsError = octokitError?.response?.data?.errors?.some((e) => 
                    e.field === 'name' && e.message?.includes('already exists')
                );
                
                if (hasNameExistsError) {
                    return {
                        success: false,
                        error: `Repository '${options.name}' already exists on this account`,
                        alreadyExists: true,
                        repositoryName: options.name
                    };
                }
            }
            
            return {
                success: false,
                error: error instanceof Error ? error.message : 'Failed to create repository'
            };
        }
    }


    /**
     * Get repository information from GitHub
     */
    static async getRepository(options: {
        owner: string;
        repo: string;
        token: string;
    }): Promise<{ success: boolean; repository?: GitHubRepository; error?: string }> {
        try {
            const octokit = GitHubService.createOctokit(options.token);
            
            const { data: repository } = await octokit.repos.get({
                owner: options.owner,
                repo: options.repo
            });

            GitHubService.logger.info('Successfully fetched repository', {
                html_url: repository.html_url
            });
            
            return { 
                success: true, 
                repository: repository as GitHubRepository 
            };
        } catch (error: unknown) {
            const octokitError = error as { status?: number; message?: string };
            
            GitHubService.logger.error('Failed to fetch repository', {
                owner: options.owner,
                repo: options.repo,
                status: octokitError?.status,
                message: octokitError?.message
            });
            return { 
                success: false, 
                error: error instanceof Error ? error.message : 'Failed to fetch repository' 
            };
        }
    }

    /**
     * Check if repository exists on GitHub
     */
    static async repositoryExists(options: {
        repositoryUrl: string;
        token: string;
    }): Promise<boolean> {
        const repoInfo = GitHubService.extractRepoInfo(options.repositoryUrl);
        
        if (!repoInfo) {
            return false;
        }

        try {
            const octokit = GitHubService.createOctokit(options.token);
            await octokit.repos.get({
                owner: repoInfo.owner,
                repo: repoInfo.repo
            });
            
            return true;
        } catch (error) {
            GitHubService.logger.error('Repository existence check failed', {
                repositoryUrl: options.repositoryUrl,
                error: error instanceof Error ? error.message : 'Unknown error'
            });
            return false;
        }
    }

    /**
     * Parse owner and repo name from GitHub URL
     */
    static extractRepoInfo(url: string): { owner: string; repo: string } | null {
        try {
            // Convert SSH URLs to HTTPS
            let cleanUrl = url;
            
            if (url.startsWith('git@github.com:')) {
                cleanUrl = url.replace('git@github.com:', 'https://github.com/');
            }
            
            const urlObj = new URL(cleanUrl);
            const pathParts = urlObj.pathname.split('/').filter(part => part);
            
            if (pathParts.length >= 2) {
                const owner = pathParts[0];
                const repo = pathParts[1].replace('.git', '');
                return { owner, repo };
            }
            
            return null;
        } catch (error) {
            GitHubService.logger.error('Failed to parse repository URL', { url, error });
            return null;
        }
    }

    /**
     * Export git repository to GitHub using native git push protocol
     * Falls back to REST API if push fails
     */
    static async exportToGitHub(options: {
        gitObjects: Array<{ path: string; data: Uint8Array }>;
        templateDetails: TemplateDetails | null;
        appQuery: string;
        appCreatedAt?: Date;
        token: string;
        repositoryUrl: string;
        username: string;
        email: string;
        useGitPush?: boolean; // Feature flag for git push vs REST API
    }): Promise<GitHubPushResponse> {
        try {
            GitHubService.logger.info('Starting GitHub export from DO git', {
                gitObjectCount: options.gitObjects.length,
                repositoryUrl: options.repositoryUrl
            });

            // Build in-memory repo from DO git objects
            const fs = await GitCloneService.buildRepository({
                gitObjects: options.gitObjects,
                templateDetails: options.templateDetails,
                appQuery: options.appQuery,
                appCreatedAt: options.appCreatedAt
            });

            // Modify README to add GitHub deploy button
            await GitHubService.modifyReadmeForGitHub(fs, options.repositoryUrl);

            // Get all commits from built repo
            const commits = await git.log({ fs, dir: '/', depth: 1000 });

            GitHubService.logger.info('Repository built', {
                commitCount: commits.length,
            });
            const pushResult = await GitHubService.pushViaGitProtocol(
                fs,
                options.token,
                options.repositoryUrl,
            );
            return pushResult;
        } catch (error) {
            GitHubService.logger.error('GitHub export failed', error);
            const errorMessage = error instanceof Error ? error.message : 'Unknown error';
            return {
                success: false,
                error: `GitHub export failed: ${errorMessage}`
            };
        }
    }

    /**
     * Export a `.git` object store verbatim to GitHub (no template rebase),
     * preserving the exact commit history. Used for think apps, whose real
     * repository lives in SpaceDO/Artifacts.
     */
    static async exportRawToGitHub(options: {
        gitObjects: Array<{ path: string; data: Uint8Array }>;
        token: string;
        repositoryUrl: string;
    }): Promise<GitHubPushResponse> {
        try {
            GitHubService.logger.info('Starting raw GitHub export', {
                gitObjectCount: options.gitObjects.length,
                repositoryUrl: options.repositoryUrl,
            });
            const fs = await GitCloneService.buildRepositoryFromObjects(options.gitObjects);
            await GitHubService.modifyReadmeForGitHub(fs, options.repositoryUrl);
            return await GitHubService.pushViaGitProtocol(fs, options.token, options.repositoryUrl);
        } catch (error) {
            GitHubService.logger.error('Raw GitHub export failed', error);
            const errorMessage = error instanceof Error ? error.message : 'Unknown error';
            return { success: false, error: `GitHub export failed: ${errorMessage}` };
        }
    }

    /**
     * Replace [cloudflarebutton] placeholder with deploy button
     */
    private static async modifyReadmeForGitHub(fs: MemFS, githubRepoUrl: string): Promise<void> {
        try {
            // Check if README exists
            try {
                await fs.stat('/README.md');
            } catch {
                GitHubService.logger.info('No README.md found, skipping modification');
                return;
            }

            const contentRaw = await fs.readFile('/README.md', { encoding: 'utf8' });
            const content = typeof contentRaw === 'string' ? contentRaw : new TextDecoder().decode(contentRaw);
            
            if (!content.includes('[cloudflarebutton]')) {
                GitHubService.logger.info('README.md has no [cloudflarebutton] placeholder');
                return;
            }

            const modified = content.replaceAll(
                '[cloudflarebutton]',
                prepareCloudflareButton(githubRepoUrl, 'markdown')
            );

            await fs.writeFile('/README.md', modified);
            await git.add({ fs, dir: '/', filepath: 'README.md' });
            await git.commit({
                fs,
                dir: '/',
                message: 'docs: Add Cloudflare deploy button to README',
                author: { 
                    name: 'vibesdk-bot', 
                    email: 'bot@vibesdk.com',
                    timestamp: Math.floor(Date.now() / 1000)
                }
            });

            GitHubService.logger.info('README.md modified and committed');
        } catch (error) {
            GitHubService.logger.warn('Failed to modify README, continuing without', error);
        }
    }

    /**
     * Normalize commit message for comparison
     */
    private static normalizeCommitMessage(message: string): string {
        return message.trim();
    }

    /**
     * Check if commit is system-generated
     */
    private static isSystemGeneratedCommit(message: string): boolean {
        const normalized = GitHubService.normalizeCommitMessage(message);
        return normalized.startsWith('docs: Add Cloudflare deploy button');
    }

    /**
     * Find last common commit between local and remote
     * Returns index in reversed (oldest-first) local commits and GitHub SHA
     */
    private static findLastCommonCommit(
        localCommits: Awaited<ReturnType<typeof git.log>>,
        remoteCommits: Array<{ sha: string; commit: { message: string } }>
    ): { lastCommonIndex: number; githubSha: string; localOid: string } | null {
        const reversedLocal = [...localCommits].reverse();
        
        // Search from newest to oldest local commit
        for (let i = reversedLocal.length - 1; i >= 0; i--) {
            const localMsg = GitHubService.normalizeCommitMessage(reversedLocal[i].commit.message);
            
            // Find matching remote commit (ignore system commits)
            const matchingRemote = remoteCommits.find(remote => {
                if (GitHubService.isSystemGeneratedCommit(remote.commit.message)) {
                    return false;
                }
                return GitHubService.normalizeCommitMessage(remote.commit.message) === localMsg;
            });
            
            if (matchingRemote) {
                return {
                    lastCommonIndex: i,
                    githubSha: matchingRemote.sha,
                    localOid: reversedLocal[i].oid
                };
            }
        }
        
        return null;
    }

    /**
     * Push to GitHub using native git push protocol
     * Much simpler and faster than REST API approach
     * Automatically handles incremental sync and packfile optimization
     */
    private static async pushViaGitProtocol(
        fs: MemFS,
        token: string,
        repositoryUrl: string,
    ): Promise<GitHubPushResponse> {
        try {
            // Extract repo info for logging
            const repoInfo = GitHubService.extractRepoInfo(repositoryUrl);
            if (!repoInfo) {
                throw new GitHubServiceError('Invalid repository URL format', 'INVALID_REPO_URL');
            }

            const { owner, repo } = repoInfo;
            
            GitHubService.logger.info('Setting up git push', {
                owner,
                repo,
                url: repositoryUrl
            });

            // Convert https://github.com/owner/repo to https://github.com/owner/repo.git
            const gitUrl = repositoryUrl.endsWith('.git') ? repositoryUrl : `${repositoryUrl}.git`;

            // Configure remote
            try {
                // Remove remote if it exists
                await git.deleteRemote({ fs, dir: '/', remote: 'github' }).catch(() => {
                    // Ignore error if remote doesn't exist
                });
            } catch {
                // Ignore
            }

            // Add fresh remote
            await git.addRemote({
                fs,
                dir: '/',
                remote: 'github',
                url: gitUrl
            });

            GitHubService.logger.info('Remote configured, starting push', {
                remote: 'github',
                url: gitUrl
            });

            // Push with force to handle sync
            // Set timeout to prevent hanging
            const PUSH_TIMEOUT_MS = 120000; // 2 minutes
            const pushPromise = git.push({
                fs,
                http, // Built-in Cloudflare Workers HTTP client
                dir: '/',
                remote: 'github',
                ref: 'main',
                force: true, // Allow non-fast-forward pushes for sync
                onAuth: () => ({
                    username: token, // GitHub accepts token as username
                    password: 'x-oauth-basic' // Or just empty string
                }),
                onAuthFailure: (url: any) => {
                    GitHubService.logger.error('Authentication failed', { url });
                },
                onAuthSuccess: (url: any) => {
                    GitHubService.logger.info('Authentication successful', { url });
                },
                onProgress: (progress: any) => {
                    GitHubService.logger.info('Push progress', {
                        phase: progress.phase,
                        loaded: progress.loaded,
                        total: progress.total
                    });
                },
                onMessage: (message) => {
                    GitHubService.logger.info('Git message', { message });
                }
            });

            // Race against timeout
            const timeoutPromise = new Promise<never>((_, reject) => {
                setTimeout(() => {
                    reject(new Error(`Git push timed out after ${PUSH_TIMEOUT_MS}ms`));
                }, PUSH_TIMEOUT_MS);
            });

            const pushResult = await Promise.race([pushPromise, timeoutPromise]);

            GitHubService.logger.info('Git push result', { pushResult });

            // Check if push was successful
            if (!pushResult.ok) {
                throw new Error(pushResult.error || 'Push failed');
            }

            // Get the final commit SHA
            const headCommit = await git.resolveRef({ fs, dir: '/', ref: 'HEAD' });

            return {
                success: true,
                commitSha: headCommit
            };
        } catch (error) {
            GitHubService.logger.error('Git push failed', { 
                error,
                errorMessage: error instanceof Error ? error.message : String(error),
                errorStack: error instanceof Error ? error.stack : undefined
            });
            throw error;
        }
    }

    /**
     * Check remote repository status vs local commits
     * Builds local repo with template to match export structure
     */
    static async checkRemoteStatus(options: {
        gitObjects: Array<{ path: string; data: Uint8Array }>;
        templateDetails: TemplateDetails | null;
        appQuery: string;
        appCreatedAt?: Date;
        repositoryUrl: string;
        token: string;
    }): Promise<{
        compatible: boolean;
        behindBy: number;
        aheadBy: number;
        divergedCommits: Array<{
            sha: string;
            message: string;
            author: string;
            date: string;
        }>;
    }> {
        try {
            const repoInfo = GitHubService.extractRepoInfo(options.repositoryUrl);
            if (!repoInfo) {
                throw new GitHubServiceError('Invalid repository URL', 'INVALID_REPO_URL');
            }

            const { owner, repo } = repoInfo;
            const octokit = GitHubService.createOctokit(options.token);

            // Get remote commits
            const { data: remoteCommits } = await octokit.repos.listCommits({
                owner,
                repo,
                per_page: 100
            });

            // Build local repo with same template as export
            const fs = await GitCloneService.buildRepository({
                gitObjects: options.gitObjects,
                templateDetails: options.templateDetails,
                appQuery: options.appQuery,
                appCreatedAt: options.appCreatedAt
            });

            const localCommits = await git.log({ fs, dir: '/', depth: 100 });

            // Find divergence
            
            // Use shared helper for finding common commits
            const commonCommit = GitHubService.findLastCommonCommit(localCommits, remoteCommits);
            const hasCommonCommit = commonCommit !== null || remoteCommits.length === 0;
            
            const localMessages = new Set(
                localCommits.map(c => GitHubService.normalizeCommitMessage(c.commit.message))
            );
            const remoteMessages = new Set(
                remoteCommits.map(c => GitHubService.normalizeCommitMessage(c.commit.message))
            );
            
            const localOnly = localCommits.filter(c => 
                !remoteMessages.has(GitHubService.normalizeCommitMessage(c.commit.message))
            );
            const remoteOnly = remoteCommits.filter(c => 
                !localMessages.has(GitHubService.normalizeCommitMessage(c.commit.message)) && 
                !GitHubService.isSystemGeneratedCommit(c.commit.message)
            );

            return {
                compatible: hasCommonCommit || remoteCommits.length === 0,
                behindBy: localOnly.length,
                aheadBy: remoteOnly.length,
                divergedCommits: remoteOnly.map(c => ({
                    sha: c.sha,
                    message: c.commit.message,
                    author: c.commit.author?.name || 'Unknown',
                    date: c.commit.author?.date || new Date().toISOString()
                }))
            };
        } catch (error) {
            GitHubService.logger.error('Failed to check remote status', error);
            throw error;
        }
    }

}