import { exec, type ExecException } from "child_process";
import { promisify } from "util";
import { writeFile, readFile, unlink } from "fs/promises";
import { Client, TextChannel } from "discord.js";
import { getPostRestartEmbed, getPostRestartProgressEmbed, type PostRestartProgress } from "@/modules/admin/update.view";
import type { PostRestartResult } from "@/modules/admin/update.view";
import type { RestartContext, UpdateCheckResult, UpdateInfo, CommitInfo } from "@/modules/admin/update.types";

const execAsync = promisify(exec);

// Constants
const STALE_CONTEXT_MS = 10 * 60 * 1000; // 10 minutes
const DEFAULT_TIMEOUT_MS = 30_000; // 30 seconds for git commands
const INSTALL_TIMEOUT_MS = 120_000; // 2 minutes for dependency installation
const BUILD_TIMEOUT_MS = 180_000; // 3 minutes for web build

/**
 * Execute a command with timeout protection
 */
async function execWithTimeout(
    cmd: string,
    timeoutMs: number = DEFAULT_TIMEOUT_MS
): Promise<{ stdout: string; stderr: string }> {
    return new Promise((resolve, reject) => {
        const process = exec(cmd, (error: ExecException | null, stdout: string, stderr: string) => {
            if (error) {
                reject(error);
            } else {
                resolve({ stdout, stderr });
            }
        });

        const timer = setTimeout(() => {
            process.kill("SIGTERM");
            reject(new Error(`Command timed out after ${timeoutMs}ms: ${cmd}`));
        }, timeoutMs);

        process.on("exit", () => clearTimeout(timer));
    });
}

export class UpdateService {
    private static readonly CONTEXT_FILE = ".restart_context.json";
    private static readonly ROLLBACK_FILE = ".rollback_commit.txt";

    // Cache for rollback state (set when we save, cleared on cleanup)
    private static rollbackPointExists: boolean | null = null;

    /**
     * Check for available updates with detailed commit information
     * Optimized: Parallel git commands and combined requirements check
     */
    static async checkForUpdates(): Promise<UpdateInfo & { requirements: UpdateCheckResult }> {
        // Get branch first (needed for subsequent commands)
        const { stdout: branchName } = await execWithTimeout("git rev-parse --abbrev-ref HEAD");
        const branch = branchName.trim();

        // Parallel execution: get current commit while fetching
        const [currentResult] = await Promise.all([
            execWithTimeout("git rev-parse --short HEAD"),
            execWithTimeout(`git fetch origin ${branch} --prune`) // Only fetch current branch
        ]);
        const currentCommit = currentResult.stdout.trim();

        // After fetch completes, get remote info in parallel
        const [latestResult, logResult, diffResult] = await Promise.all([
            execWithTimeout(`git rev-parse --short origin/${branch}`),
            execWithTimeout(`git log HEAD..origin/${branch} --format="%h|%s|%an" --no-merges`),
            execWithTimeout(`git diff HEAD..origin/${branch} --name-only`)
        ]);

        const latestCommit = latestResult.stdout.trim();

        // Parse commit log
        const commits: CommitInfo[] = logResult.stdout
            .trim()
            .split("\n")
            .filter(line => line.length > 0)
            .map(line => {
                const [hash, message, author] = line.split("|");
                return { hash: hash || "", message: message || "", author: author || "" };
            });

        // Parse changed files and analyze requirements in one pass
        const changedFiles = diffResult.stdout.trim().split("\n").filter(f => f.length > 0);
        const requirements = this.analyzeChangedFiles(changedFiles);

        return {
            hasUpdates: commits.length > 0,
            branch,
            currentCommit,
            latestCommit,
            commitCount: commits.length,
            commits,
            requirements
        };
    }

    /**
     * Analyze changed files to determine update requirements
     * Extracted for reuse and clarity
     */
    private static analyzeChangedFiles(changedFiles: string[]): UpdateCheckResult {
        const needsRootInstall = changedFiles.some(file =>
            file === "package.json" || file === "bun.lock"
        );

        const needsWebInstall = changedFiles.some(file =>
            file === "web/package.json" || file === "web/bun.lock"
        );

        // Only rebuild web if essential source files changed
        const needsWebBuild = changedFiles.some(file =>
            file.match(/^web\/src\/(components|pages|lib|index)/) ||
            file === "web/build.ts" ||
            file === "web/tailwind.config.ts" ||
            file === "web/tsconfig.json"
        );

        const needsMigrations = changedFiles.some(file =>
            file.includes("schema.ts") || file.startsWith("drizzle/")
        );

        return {
            needsRootInstall,
            needsWebInstall,
            needsWebBuild,
            needsMigrations,
            changedFiles
        };
    }

    /**
     * @deprecated Use checkForUpdates() which now includes requirements
     * Kept for backwards compatibility
     */
    static async checkUpdateRequirements(branch: string): Promise<UpdateCheckResult> {
        try {
            const { stdout } = await execWithTimeout(`git diff HEAD..origin/${branch} --name-only`);
            const changedFiles = stdout.trim().split("\n").filter(f => f.length > 0);
            return this.analyzeChangedFiles(changedFiles);
        } catch (e) {
            console.error("Failed to check update requirements:", e);
            return {
                needsRootInstall: false,
                needsWebInstall: false,
                needsWebBuild: false,
                needsMigrations: false,
                changedFiles: [],
                error: e instanceof Error ? e : new Error(String(e))
            };
        }
    }

    /**
     * Get a summary of changed file categories
     */
    static categorizeChanges(changedFiles: string[]): Record<string, number> {
        const categories: Record<string, number> = {};

        for (const file of changedFiles) {
            let category = "Other";

            if (file.startsWith("bot/commands/")) category = "Commands";
            else if (file.startsWith("bot/modules/")) category = "Modules";
            else if (file.startsWith("web/")) category = "Web Dashboard";
            else if (file.startsWith("bot/lib/") || file.startsWith("shared/lib/")) category = "Library";
            else if (file.startsWith("drizzle/") || file.includes("schema")) category = "Database";
            else if (file.endsWith(".test.ts")) category = "Tests";
            else if (file.includes("package.json") || file.includes("lock")) category = "Dependencies";

            categories[category] = (categories[category] || 0) + 1;
        }

        return categories;
    }

    /**
     * Save the current commit for potential rollback
     */
    static async saveRollbackPoint(): Promise<string> {
        const { stdout } = await execWithTimeout("git rev-parse HEAD");
        const commit = stdout.trim();
        await writeFile(this.ROLLBACK_FILE, commit);
        this.rollbackPointExists = true; // Cache the state
        return commit;
    }

    /**
     * Rollback to the previous commit
     */
    static async rollback(): Promise<{ success: boolean; message: string }> {
        try {
            const rollbackCommit = await readFile(this.ROLLBACK_FILE, "utf-8");
            await execWithTimeout(`git reset --hard ${rollbackCommit.trim()}`);
            await unlink(this.ROLLBACK_FILE);
            this.rollbackPointExists = false;
            return { success: true, message: `Rolled back to ${rollbackCommit.trim().substring(0, 7)}` };
        } catch (e) {
            return {
                success: false,
                message: e instanceof Error ? e.message : "No rollback point available"
            };
        }
    }

    /**
     * Check if a rollback point exists
     * Uses cache when available to avoid file system access
     */
    static async hasRollbackPoint(): Promise<boolean> {
        if (this.rollbackPointExists !== null) {
            return this.rollbackPointExists;
        }
        try {
            await readFile(this.ROLLBACK_FILE, "utf-8");
            this.rollbackPointExists = true;
            return true;
        } catch {
            this.rollbackPointExists = false;
            return false;
        }
    }

    /**
     * Perform the git update
     */
    static async performUpdate(branch: string): Promise<void> {
        await execWithTimeout(`git reset --hard origin/${branch}`);
    }

    /**
     * Install dependencies for specified projects
     * Optimized: Parallel installation
     */
    static async installDependencies(options: { root: boolean; web: boolean }): Promise<string> {
        const tasks: Promise<{ label: string; output: string }>[] = [];

        if (options.root) {
            tasks.push(
                execWithTimeout("bun install", INSTALL_TIMEOUT_MS)
                    .then(({ stdout }) => ({ label: "📦 Root", output: stdout.trim() || "Done" }))
            );
        }

        if (options.web) {
            tasks.push(
                execWithTimeout("cd web && bun install", INSTALL_TIMEOUT_MS)
                    .then(({ stdout }) => ({ label: "🌐 Web", output: stdout.trim() || "Done" }))
            );
        }

        const results = await Promise.all(tasks);
        return results.map(r => `${r.label}: ${r.output}`).join("\n");
    }

    /**
     * Prepare restart context with rollback info
     */
    static async prepareRestartContext(context: RestartContext): Promise<void> {
        await writeFile(this.CONTEXT_FILE, JSON.stringify(context));
    }

    /**
     * Trigger a restart
     */
    static async triggerRestart(): Promise<void> {
        if (process.env.RESTART_COMMAND) {
            exec(process.env.RESTART_COMMAND).unref();
        } else {
            setTimeout(() => process.exit(0), 100);
        }
    }

    /**
     * Handle post-restart tasks
     */
    static async handlePostRestart(client: Client): Promise<void> {
        try {
            const context = await this.loadRestartContext();
            if (!context) return;

            if (this.isContextStale(context)) {
                await this.cleanupContext();
                return;
            }

            const channel = await this.fetchNotificationChannel(client, context.channelId);
            if (!channel) {
                await this.cleanupContext();
                return;
            }

            const result = await this.executePostRestartTasks(context, channel);
            await this.notifyPostRestartResult(channel, result);
            await this.cleanupContext();
        } catch (e) {
            console.error("Failed to handle post-restart context:", e);
        }
    }

    // --- Private Helper Methods ---

    private static async loadRestartContext(): Promise<RestartContext | null> {
        try {
            const contextData = await readFile(this.CONTEXT_FILE, "utf-8");
            return JSON.parse(contextData) as RestartContext;
        } catch {
            return null;
        }
    }

    private static isContextStale(context: RestartContext): boolean {
        return Date.now() - context.timestamp > STALE_CONTEXT_MS;
    }

    private static async fetchNotificationChannel(client: Client, channelId: string): Promise<TextChannel | null> {
        try {
            const channel = await client.channels.fetch(channelId);
            if (channel && channel.isSendable() && channel instanceof TextChannel) {
                return channel;
            }
            return null;
        } catch {
            return null;
        }
    }

    private static async executePostRestartTasks(
        context: RestartContext,
        channel: TextChannel
    ): Promise<PostRestartResult> {
        const result: PostRestartResult = {
            installSuccess: true,
            installOutput: "",
            webBuildSuccess: true,
            webBuildOutput: "",
            migrationSuccess: true,
            migrationOutput: "",
            ranInstall: context.installDependencies,
            ranWebBuild: context.buildWebAssets,
            ranMigrations: context.runMigrations,
            previousCommit: context.previousCommit,
            newCommit: context.newCommit
        };

        // Track progress for consolidated message
        const progress: PostRestartProgress = {
            installDeps: context.installDependencies,
            buildWeb: context.buildWebAssets,
            runMigrations: context.runMigrations,
            currentStep: "starting"
        };

        // Only send progress message if there are tasks to run
        const hasTasks = context.installDependencies || context.buildWebAssets || context.runMigrations;
        let progressMessage = hasTasks
            ? await channel.send({ embeds: [getPostRestartProgressEmbed(progress)] })
            : null;

        // Helper to update progress message
        const updateProgress = async () => {
            if (progressMessage) {
                await progressMessage.edit({ embeds: [getPostRestartProgressEmbed(progress)] });
            }
        };

        // 1. Install Dependencies if needed (PARALLELIZED)
        if (context.installDependencies) {
            try {
                progress.currentStep = "install";
                await updateProgress();

                // Parallel installation of root and web dependencies
                const [rootResult, webResult] = await Promise.all([
                    execWithTimeout("bun install", INSTALL_TIMEOUT_MS)
                        .then(({ stdout }) => ({ success: true, output: stdout.trim() || "Done" }))
                        .catch(err => ({ success: false, output: err instanceof Error ? err.message : String(err) })),
                    execWithTimeout("cd web && bun install", INSTALL_TIMEOUT_MS)
                        .then(({ stdout }) => ({ success: true, output: stdout.trim() || "Done" }))
                        .catch(err => ({ success: false, output: err instanceof Error ? err.message : String(err) }))
                ]);

                result.installSuccess = rootResult.success && webResult.success;
                result.installOutput = `📦 Root: ${rootResult.output}\n🌐 Web: ${webResult.output}`;
                progress.installDone = true;

                if (!result.installSuccess) {
                    console.error("Dependency Install Failed:", result.installOutput);
                }
            } catch (err: unknown) {
                result.installSuccess = false;
                result.installOutput = err instanceof Error ? err.message : String(err);
                progress.installDone = true;
                console.error("Dependency Install Failed:", err);
            }
        }

        // 2. Build Web Assets if needed
        if (context.buildWebAssets) {
            try {
                progress.currentStep = "build";
                await updateProgress();

                const { stdout } = await execWithTimeout("cd web && bun run build", BUILD_TIMEOUT_MS);
                result.webBuildOutput = stdout.trim() || "Build completed successfully";
                progress.buildDone = true;
            } catch (err: unknown) {
                result.webBuildSuccess = false;
                result.webBuildOutput = err instanceof Error ? err.message : String(err);
                progress.buildDone = true;
                console.error("Web Build Failed:", err);
            }
        }

        // 3. Run Migrations
        if (context.runMigrations) {
            try {
                progress.currentStep = "migrate";
                await updateProgress();

                const { stdout } = await execWithTimeout("bun x drizzle-kit migrate", DEFAULT_TIMEOUT_MS);
                result.migrationOutput = stdout;
                progress.migrateDone = true;
            } catch (err: unknown) {
                result.migrationSuccess = false;
                result.migrationOutput = err instanceof Error ? err.message : String(err);
                progress.migrateDone = true;
                console.error("Migration Failed:", err);
            }
        }

        // Delete progress message before final result
        if (progressMessage) {
            try {
                await progressMessage.delete();
            } catch {
                // Message may already be deleted, ignore
            }
        }

        return result;
    }

    private static async notifyPostRestartResult(
        channel: TextChannel,
        result: PostRestartResult
    ): Promise<void> {
        // Use cached rollback state - we just saved it before restart
        const hasRollback = await this.hasRollbackPoint();
        await channel.send(getPostRestartEmbed(result, hasRollback));
    }

    private static async cleanupContext(): Promise<void> {
        try {
            await unlink(this.CONTEXT_FILE);
        } catch {
            // File may not exist, ignore
        }
        // Don't clear rollback cache here - rollback file persists
    }
}
