import { BaseController } from '../baseController';
import { RouteContext } from '../../types/route-context';
import { GitHubService } from '../../../services/github';
import { GitHubExporterOAuthProvider } from '../../../services/oauth/github-exporter';
import { getAgentStub, getSpaceGitStub, isThinkApp } from '../../../agents';
import { createLogger } from '../../../logger';
import { AppService } from '../../../database/services/AppService';
import { ExportResult } from 'worker/agents/core/types';
import { SignJWT, jwtVerify, JWTPayload } from 'jose';
import { validateRedirectUrl } from '../../../utils/authUtils';

export interface GitHubExportData {
    success: boolean;
    repositoryUrl?: string;
    error?: string;
}

interface GitHubExportStatePayload extends JWTPayload {
    userId: string;
    purpose: 'repository_export';
    agentId: string;
    returnUrl: string;
    exportData: {
        repositoryName: string;
        description?: string;
        isPrivate?: boolean;
    };
}

export class GitHubExporterController extends BaseController {
    static readonly logger = createLogger('GitHubExporterController');
    private static readonly STATE_EXPIRY = '10m';

    private static async signState(payload: Omit<GitHubExportStatePayload, 'iat' | 'exp'>, secret: string): Promise<string> {
        return new SignJWT(payload)
            .setProtectedHeader({ alg: 'HS256' })
            .setIssuedAt()
            .setExpirationTime(this.STATE_EXPIRY)
            .sign(new TextEncoder().encode(secret));
    }

    private static async verifyState(token: string, secret: string): Promise<GitHubExportStatePayload | null> {
        try {
            const { payload } = await jwtVerify(
                token,
                new TextEncoder().encode(secret)
            );
            return payload as GitHubExportStatePayload;
        } catch (error) {
            this.logger.warn('State verification failed', { error });
            return null;
        }
    }

    /**
     * Creates GitHub repository and pushes files from agent
     * If existingRepositoryUrl is provided, skips creation and syncs to existing repo
     */
    private static async createRepositoryAndPush(options: {
        env: Env;
        agentId: string;
        repositoryName: string;
        description?: string;
        isPrivate: boolean;
        token: string;
        username: string;
        existingRepositoryUrl?: string;
    }): Promise<
        | { success: true; repositoryUrl: string } 
        | { success: false; error: string; alreadyExists?: boolean; existingRepositoryUrl?: string }
    > {
        const { env, agentId, repositoryName, description, isPrivate, token, username, existingRepositoryUrl } = options;
        
        try {
            let repositoryUrl: string | undefined;
            let cloneUrl: string | undefined;
            
            // Check database for existing repository if not provided
            let finalExistingRepoUrl = existingRepositoryUrl;
            if (!finalExistingRepoUrl) {
                try {
                    const appService = new AppService(env);
                    const app = await appService.getAppDetails(agentId);
                    finalExistingRepoUrl = app?.githubRepositoryUrl || undefined;
                    
                    if (finalExistingRepoUrl) {
                        this.logger.info('Found existing GitHub repository in database', { 
                            agentId, 
                            repositoryUrl: finalExistingRepoUrl 
                        });
                    }
                } catch (error) {
                    this.logger.warn('Failed to check for existing repository', { error, agentId });
                }
            }
            
            // Determine repository details (sync to existing or create new)
            if (finalExistingRepoUrl) {
                // Check if repository still exists on GitHub
                const exists = await GitHubService.repositoryExists({
                    repositoryUrl: finalExistingRepoUrl,
                    token
                });
                
                if (!exists) {
                    // Repository doesn't exist - clear from database and create new
                    this.logger.info('Repository no longer exists, creating new one', {
                        agentId,
                        oldUrl: finalExistingRepoUrl,
                        repositoryName
                    });
                    
                    try {
                        const appService = new AppService(env);
                        await appService.updateGitHubRepository(agentId, '', 'public');
                    } catch (clearError) {
                        this.logger.warn('Failed to clear repository URL', { error: clearError, agentId });
                    }
                    
                    // Create new repository
                    finalExistingRepoUrl = undefined;
                } else {
                    // Repository exists, use it
                    repositoryUrl = finalExistingRepoUrl;
                    cloneUrl = finalExistingRepoUrl.endsWith('.git') 
                        ? finalExistingRepoUrl 
                        : `${finalExistingRepoUrl}.git`;
                }
            }
            
            if (!finalExistingRepoUrl) {
                this.logger.info('Creating new repository', { agentId, repositoryName });
                
                const createResult = await GitHubService.createUserRepository({
                    name: repositoryName,
                    description,
                    private: isPrivate,
                    token
                });

                if (!createResult.success) {
                    // Repository already exists on GitHub - fetch the actual repository
                    if (createResult.alreadyExists && createResult.repositoryName) {
                        this.logger.warn('Repository already exists, fetching repository info', { 
                            agentId, 
                            repositoryName: createResult.repositoryName 
                        });
                        
                        const repoResult = await GitHubService.getRepository({
                            owner: username,
                            repo: createResult.repositoryName,
                            token
                        });
                        
                        if (repoResult.success && repoResult.repository) {
                            return { 
                                success: false, 
                                error: 'Repository already exists',
                                alreadyExists: true,
                                existingRepositoryUrl: repoResult.repository.html_url
                            };
                        }
                    }
                    
                    return { success: false, error: createResult.error || 'Repository creation failed' };
                }

                if (!createResult.repository) {
                    return { success: false, error: 'Repository creation failed' };
                }

                const { repository } = createResult;
                repositoryUrl = repository.html_url;
                cloneUrl = repository.clone_url;
                
                this.logger.info('Repository created', { agentId, repositoryUrl });
            }

            // Ensure repository URLs are set
            if (!repositoryUrl || !cloneUrl) {
                return { 
                    success: false, 
                    error: 'Failed to determine repository URLs' 
                };
            }

            // Push files to repository
            this.logger.info('Pushing files to repository', { agentId, repositoryUrl });

            // Think apps: the real repository (with full history) lives in
            // SpaceDO/Artifacts, not the agent's own git. Push it verbatim.
            if (await isThinkApp(env, agentId)) {
                const spaceStub = getSpaceGitStub(env, agentId);
                if (!spaceStub) {
                    return { success: false, error: 'Workspace unavailable for export' };
                }
                const gitObjects = await spaceStub.exportGitObjects();
                if (gitObjects.length === 0) {
                    return { success: false, error: 'Repository has no commits to export yet' };
                }
                const rawResult = await GitHubService.exportRawToGitHub({
                    gitObjects,
                    token,
                    repositoryUrl: cloneUrl,
                });
                if (!rawResult.success) {
                    return { success: false, error: rawResult.error || 'File push failed' };
                }
                this.logger.info('Think export completed', { agentId, repositoryUrl });
                return { success: true, repositoryUrl };
            }

            const agentStub = await getAgentStub(env, agentId);
            const pushResult: ExportResult = await agentStub.exportProject({
                kind: 'github',
                github: {
                    cloneUrl,
                    repositoryHtmlUrl: repositoryUrl,
                    isPrivate,
                    token,
                    email: 'vibesdk-bot@cloudflare.com',
                    username
                }
            });

            if (!pushResult?.success) {
                return { 
                    success: false, 
                    error: pushResult?.error || (finalExistingRepoUrl ? 'Sync failed' : 'File push failed')
                };
            }

            const operationType = finalExistingRepoUrl ? 'Sync' : 'Export';
            this.logger.info(`${operationType} completed`, { agentId, repositoryUrl });
            
            return { success: true, repositoryUrl };
        } catch (error) {
            this.logger.error('Repository operation failed', { error, agentId, repositoryName });
            return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
        }
    }

    static async handleOAuthCallback(
        request: Request,
        env: Env,
        _ctx: ExecutionContext,
        context: RouteContext,
    ): Promise<Response> {
        try {
            const code = context.queryParams.get('code');
            const stateParam = context.queryParams.get('state');
            const error = context.queryParams.get('error');

            if (error) {
                this.logger.error('OAuth authorization error', { error });
                return Response.redirect(
                    `${new URL(request.url).origin}/settings?integration=github&status=error&reason=${encodeURIComponent(error)}`,
                    302,
                );
            }

            if (!code) {
                return Response.redirect(
                    `${new URL(request.url).origin}/settings?integration=github&status=error&reason=missing_code`,
                    302,
                );
            }

            if (!stateParam) {
                return Response.redirect(
                    `${new URL(request.url).origin}/settings?integration=github&status=error&reason=missing_state`,
                    302,
                );
            }

            const parsedState = await this.verifyState(stateParam, env.JWT_SECRET);

            if (!parsedState) {
                this.logger.warn('Invalid or expired OAuth state');
                return Response.redirect(
                    `${new URL(request.url).origin}/settings?integration=github&status=error&reason=invalid_state`,
                    302,
                );
            }

            const { userId, purpose, agentId, exportData, returnUrl } = parsedState;

            if (!context.user || context.user.id !== userId) {
                this.logger.warn('Session user mismatch in OAuth callback', {
                    stateUserId: userId,
                    sessionUserId: context.user?.id,
                });
                return Response.redirect(
                    `${new URL(request.url).origin}/settings?integration=github&status=error&reason=session_mismatch`,
                    302,
                );
            }

            const validatedReturnUrl = validateRedirectUrl(returnUrl, request)
                || `${new URL(request.url).origin}/chat`;

            const baseUrl = new URL(request.url).origin;
            const oauthProvider = GitHubExporterOAuthProvider.create(env, baseUrl);

            const tokenResult = await oauthProvider.exchangeCodeForTokens(code);

            if (!tokenResult || !tokenResult.accessToken) {
                this.logger.error('Failed to exchange OAuth code', { userId });
                
                return Response.redirect(
                    `${validatedReturnUrl}?github_export=error&reason=token_exchange_failed`,
                    302,
                );
            }

            this.logger.info('OAuth authorization successful', {
                userId,
                purpose
            });

            if (purpose === 'repository_export') {
                const appService = new AppService(env);
                const ownershipResult = await appService.checkAppOwnership(agentId, userId);
                if (!ownershipResult.isOwner) {
                    this.logger.warn('OAuth callback ownership check failed', { userId, agentId });
                    return Response.redirect(
                        `${validatedReturnUrl}?github_export=error&reason=${encodeURIComponent('You do not have permission to export this app')}`,
                        302,
                    );
                }

                const result = await this.createRepositoryAndPush({
                    env,
                    agentId,
                    repositoryName: exportData.repositoryName,
                    description: exportData.description,
                    isPrivate: exportData.isPrivate || false,
                    token: tokenResult.accessToken,
                    username: 'vibesdk-bot'
                });

                if (!result.success) {
                    return Response.redirect(
                        `${validatedReturnUrl}?github_export=error&reason=${encodeURIComponent(result.error)}`,
                        302,
                    );
                }

                this.logger.info('OAuth export completed', { userId, agentId, repositoryUrl: result.repositoryUrl });

                return Response.redirect(
                    `${validatedReturnUrl}?github_export=success&repository_url=${encodeURIComponent(result.repositoryUrl)}`,
                    302,
                );
            }

            return Response.redirect(
                `${validatedReturnUrl}?integration=github&status=oauth_success`,
                302,
            );
        } catch (error) {
            this.logger.error('Failed to handle OAuth callback', error);
            return Response.redirect(
                `${new URL(request.url).origin}/settings?integration=github&status=error`,
                302,
            );
        }
    }

    static async initiateGitHubExport(
        request: Request,
        env: Env,
        _ctx: ExecutionContext,
        context: RouteContext,
    ): Promise<Response> {
        try {
            if (!context.user) {
                return GitHubExporterController.createErrorResponse<never>(
                    'Authentication required',
                    401,
                );
            }

            const body = await request.json() as {
                repositoryName: string;
                description?: string;
                isPrivate?: boolean;
                agentId: string;
            };

            const appService = new AppService(env);
            const ownershipResult = await appService.checkAppOwnership(body.agentId, context.user.id);
            if (!ownershipResult.isOwner) {
                return GitHubExporterController.createErrorResponse<never>(
                    'You do not have permission to access this app',
                    403
                );
            }

            this.logger.info('Export initiated', { userId: context.user.id, agentId: body.agentId });

            if (!body.repositoryName) {
                return GitHubExporterController.createErrorResponse<never>(
                    'Repository name is required',
                    400,
                );
            }

            if (!body.agentId) {
                return GitHubExporterController.createErrorResponse<never>(
                    'Instance ID is required for file pushing',
                    400,
                );
            }

            const agentStub = await getAgentStub(env, body.agentId);
            const cachedToken = await agentStub.getGitHubToken();
            
            if (cachedToken) {
                this.logger.info('Using cached token', { agentId: body.agentId, username: cachedToken.username });
                
                const result = await this.createRepositoryAndPush({
                    env,
                    agentId: body.agentId,
                    repositoryName: body.repositoryName,
                    description: body.description,
                    isPrivate: body.isPrivate ?? false,
                    token: cachedToken.token,
                    username: cachedToken.username
                });
                
                if (result.success) {
                    this.logger.info('Direct export completed', { repositoryUrl: result.repositoryUrl, agentId: body.agentId });
                    return GitHubExporterController.createSuccessResponse({
                        success: true,
                        repositoryUrl: result.repositoryUrl,
                        skippedOAuth: true
                    });
                }
                
                const isTemporaryError = result.error?.includes('rate limit') || 
                                        result.error?.includes('timeout') ||
                                        result.error?.includes('ECONNRESET');
                
                if (isTemporaryError) {
                    this.logger.warn('Temporary error, keeping token', { error: result.error, agentId: body.agentId });
                    return GitHubExporterController.createErrorResponse(
                        result.error || 'Temporary GitHub error',
                        503
                    );
                }
            } else {
                this.logger.info('No cached token, initiating OAuth', { agentId: body.agentId });
            }

            const baseUrl = new URL(request.url).origin;
            const rawReturnUrl = request.headers.get('referer') || `${baseUrl}/chat`;
            const validatedReturnUrl = validateRedirectUrl(rawReturnUrl, request) || `${baseUrl}/chat`;

            const statePayload: Omit<GitHubExportStatePayload, 'iat' | 'exp'> = {
                userId: context.user.id,
                purpose: 'repository_export',
                agentId: body.agentId,
                exportData: {
                    repositoryName: body.repositoryName,
                    description: body.description,
                    isPrivate: body.isPrivate
                },
                returnUrl: validatedReturnUrl,
            };

            const signedState = await this.signState(statePayload, env.JWT_SECRET);
            const oauthProvider = GitHubExporterOAuthProvider.create(env, baseUrl);

            const authUrl = await oauthProvider.getAuthorizationUrl(signedState);

            this.logger.info('Initiating OAuth flow', { userId: context.user.id, agentId: body.agentId });

            return GitHubExporterController.createSuccessResponse<{ authUrl: string }>({
                authUrl
            });
        } catch (error) {
            this.logger.error('Failed to initiate GitHub export', error);
            return GitHubExporterController.createErrorResponse<never>(
                'Failed to initiate GitHub export',
                500,
            );
        }
    }

    static async checkRemoteStatus(
        request: Request,
        env: Env,
        _ctx: ExecutionContext,
        context: RouteContext,
    ): Promise<Response> {
        try {
            if (!context.user) {
                return GitHubExporterController.createErrorResponse<never>(
                    'Authentication required',
                    401
                );
            }

            const body = await request.json() as {
                repositoryUrl: string;
                agentId: string;
            };

            if (!body.repositoryUrl || !body.agentId) {
                return GitHubExporterController.createErrorResponse<never>(
                    'Repository URL and agent ID are required',
                    400
                );
            }

            const appService = new AppService(env);
            const ownershipResult = await appService.checkAppOwnership(body.agentId, context.user.id);
            if (!ownershipResult.isOwner) {
                return GitHubExporterController.createErrorResponse<never>(
                    'You do not have permission to access this app',
                    403
                );
            }

            const agentStub = await getAgentStub(env, body.agentId);
            
            // Try to get cached token
            const cachedToken = await agentStub.getGitHubToken();
            
            if (!cachedToken) {
                return GitHubExporterController.createErrorResponse<never>(
                    'No cached GitHub token. Please re-authenticate.',
                    401
                );
            }

            // Export git objects and template details
            const { gitObjects, query, templateDetails } = await agentStub.exportGitObjects();
            
            // Get app createdAt
            let appCreatedAt: Date | undefined;
            try {
                const appService = new AppService(env);
                const app = await appService.getAppDetails(body.agentId);
                if (app && app.createdAt) {
                    appCreatedAt = new Date(app.createdAt);
                }
            } catch (error) {
                this.logger.warn('Failed to get app createdAt for sync check', { error });
            }
            
            // Check remote status
            const status = await GitHubService.checkRemoteStatus({
                gitObjects,
                templateDetails,
                appQuery: query,
                appCreatedAt,
                repositoryUrl: body.repositoryUrl,
                token: cachedToken.token
            });

            return GitHubExporterController.createSuccessResponse(status);
        } catch (error) {
            this.logger.error('Failed to check remote status', error);
            return GitHubExporterController.createErrorResponse<never>(
                'Failed to check remote status',
                500
            );
        }
    }
}
