import { CommandModule } from "yargs";
import { db, idpOidcConfig, licenseKey, certificates, eventStreamingDestinations, alertWebhookActions } from "@server/db";
import { encrypt, decrypt } from "@server/lib/crypto";
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
import { eq } from "drizzle-orm";
import fs from "fs";
import yaml from "js-yaml";

type RotateServerSecretArgs = {
    "old-secret": string;
    "new-secret": string;
    force?: boolean;
};

export const rotateServerSecret: CommandModule<
    {},
    RotateServerSecretArgs
> = {
    command: "rotate-server-secret",
    describe:
        "Rotate the server secret by decrypting all encrypted values with the old secret and re-encrypting with a new secret",
    builder: (yargs) => {
        return yargs
            .option("old-secret", {
                type: "string",
                demandOption: true,
                describe: "The current server secret (for verification)"
            })
            .option("new-secret", {
                type: "string",
                demandOption: true,
                describe: "The new server secret to use"
            })
            .option("force", {
                type: "boolean",
                default: false,
                describe:
                    "Force rotation even if the old secret doesn't match the config file. " +
                    "Use this if you know the old secret is correct but the config file is out of sync. " +
                    "WARNING: This will attempt to decrypt all values with the provided old secret. " +
                    "If the old secret is incorrect, the rotation will fail or corrupt data."
            });
    },
    handler: async (argv: {
        "old-secret": string;
        "new-secret": string;
        force?: boolean;
    }) => {
        try {
            // Determine which config file exists
            const configPath = fs.existsSync(configFilePath1)
                ? configFilePath1
                : fs.existsSync(configFilePath2)
                  ? configFilePath2
                  : null;

            if (!configPath) {
                console.error(
                    "Error: Config file not found. Expected config.yml or config.yaml in the config directory."
                );
                process.exit(1);
            }

            // Read current config
            const configContent = fs.readFileSync(configPath, "utf8");
            const config = yaml.load(configContent) as any;

            if (!config?.server?.secret) {
                console.error(
                    "Error: No server secret found in config file. Cannot rotate."
                );
                process.exit(1);
            }

            const configSecret = config.server.secret;
            const oldSecret = argv["old-secret"];
            const newSecret = argv["new-secret"];
            const force = argv.force || false;

            // Verify that the provided old secret matches the one in config
            if (configSecret !== oldSecret) {
                if (!force) {
                    console.error(
                        "Error: The provided old secret does not match the secret in the config file."
                    );
                    console.error(
                        "\nIf you are certain the old secret is correct and the config file is out of sync,"
                    );
                    console.error(
                        "you can use the --force flag to bypass this check."
                    );
                    console.error(
                        "\nWARNING: Using --force with an incorrect old secret will cause the rotation to fail"
                    );
                    console.error(
                        "or corrupt encrypted data. Only use --force if you are absolutely certain."
                    );
                    process.exit(1);
                } else {
                    console.warn(
                        "\nWARNING: Using --force flag. Bypassing old secret verification."
                    );
                    console.warn(
                        "The provided old secret does not match the config file, but proceeding anyway."
                    );
                    console.warn(
                        "If the old secret is incorrect, this operation will fail or corrupt data.\n"
                    );
                }
            }

            // Validate new secret
            if (newSecret.length < 8) {
                console.error(
                    "Error: New secret must be at least 8 characters long"
                );
                process.exit(1);
            }

            if (oldSecret === newSecret) {
                console.error("Error: New secret must be different from old secret");
                process.exit(1);
            }

            console.log("Starting server secret rotation...");
            console.log("This will decrypt and re-encrypt all encrypted values in the database.");

            // Read all data first
            console.log("\nReading encrypted data from database...");
            const idpConfigs = await db.select().from(idpOidcConfig);
            const licenseKeys = await db.select().from(licenseKey);
            const certs = await db.select().from(certificates);
            const streamingDestinations = await db.select().from(eventStreamingDestinations);
            const webhookActions = await db.select().from(alertWebhookActions);

            console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`);
            console.log(`Found ${licenseKeys.length} license key(s)`);
            console.log(`Found ${certs.length} certificate(s)`);
            console.log(`Found ${streamingDestinations.length} event streaming destination(s)`);
            console.log(`Found ${webhookActions.length} alert webhook action(s)`);

            // Prepare all decrypted and re-encrypted values
            console.log("\nDecrypting and re-encrypting values...");

            type IdpUpdate = {
                idpOauthConfigId: number;
                encryptedClientId: string;
                encryptedClientSecret: string;
            };

            type LicenseKeyUpdate = {
                oldLicenseKeyId: string;
                newLicenseKeyId: string;
                encryptedToken: string;
                encryptedInstanceId: string;
            };

            type CertUpdate = {
                certId: number;
                encryptedCertFile: string | null;
                encryptedKeyFile: string | null;
            };

            type StreamingDestinationUpdate = {
                destinationId: number;
                encryptedConfig: string;
            };

            type WebhookActionUpdate = {
                webhookActionId: number;
                encryptedConfig: string;
            };

            const idpUpdates: IdpUpdate[] = [];
            const licenseKeyUpdates: LicenseKeyUpdate[] = [];
            const certUpdates: CertUpdate[] = [];
            const streamingDestinationUpdates: StreamingDestinationUpdate[] = [];
            const webhookActionUpdates: WebhookActionUpdate[] = [];

            // Process idpOidcConfig entries
            for (const idpConfig of idpConfigs) {
                try {
                    // Decrypt with old secret
                    const decryptedClientId = decrypt(idpConfig.clientId, oldSecret);
                    const decryptedClientSecret = decrypt(
                        idpConfig.clientSecret,
                        oldSecret
                    );

                    // Re-encrypt with new secret
                    const encryptedClientId = encrypt(decryptedClientId, newSecret);
                    const encryptedClientSecret = encrypt(
                        decryptedClientSecret,
                        newSecret
                    );

                    idpUpdates.push({
                        idpOauthConfigId: idpConfig.idpOauthConfigId,
                        encryptedClientId,
                        encryptedClientSecret
                    });
                } catch (error) {
                    console.error(
                        `Error processing IdP config ${idpConfig.idpOauthConfigId}:`,
                        error
                    );
                    throw error;
                }
            }

            // Process licenseKey entries
            for (const key of licenseKeys) {
                try {
                    // Decrypt with old secret
                    const decryptedLicenseKeyId = decrypt(key.licenseKeyId, oldSecret);
                    const decryptedToken = decrypt(key.token, oldSecret);
                    const decryptedInstanceId = decrypt(key.instanceId, oldSecret);

                    // Re-encrypt with new secret
                    const encryptedLicenseKeyId = encrypt(
                        decryptedLicenseKeyId,
                        newSecret
                    );
                    const encryptedToken = encrypt(decryptedToken, newSecret);
                    const encryptedInstanceId = encrypt(
                        decryptedInstanceId,
                        newSecret
                    );

                    licenseKeyUpdates.push({
                        oldLicenseKeyId: key.licenseKeyId,
                        newLicenseKeyId: encryptedLicenseKeyId,
                        encryptedToken,
                        encryptedInstanceId
                    });
                } catch (error) {
                    console.error(
                        `Error processing license key ${key.licenseKeyId}:`,
                        error
                    );
                    throw error;
                }
            }

            // Process certificate entries
            for (const cert of certs) {
                try {
                    const encryptedCertFile = cert.certFile
                        ? encrypt(decrypt(cert.certFile, oldSecret), newSecret)
                        : null;
                    const encryptedKeyFile = cert.keyFile
                        ? encrypt(decrypt(cert.keyFile, oldSecret), newSecret)
                        : null;

                    certUpdates.push({
                        certId: cert.certId,
                        encryptedCertFile,
                        encryptedKeyFile
                    });
                } catch (error) {
                    console.error(
                        `Error processing certificate ${cert.certId} (${cert.domain}):`,
                        error
                    );
                    throw error;
                }
            }

            // Process eventStreamingDestinations entries
            for (const dest of streamingDestinations) {
                try {
                    const decryptedConfig = decrypt(dest.config, oldSecret);
                    const encryptedConfig = encrypt(decryptedConfig, newSecret);

                    streamingDestinationUpdates.push({
                        destinationId: dest.destinationId,
                        encryptedConfig
                    });
                } catch (error) {
                    console.error(
                        `Error processing event streaming destination ${dest.destinationId}:`,
                        error
                    );
                    throw error;
                }
            }

            // Process alertWebhookActions entries
            for (const webhook of webhookActions) {
                try {
                    if (webhook.config == null) continue;

                    const decryptedConfig = decrypt(webhook.config, oldSecret);
                    const encryptedConfig = encrypt(decryptedConfig, newSecret);

                    webhookActionUpdates.push({
                        webhookActionId: webhook.webhookActionId,
                        encryptedConfig
                    });
                } catch (error) {
                    console.error(
                        `Error processing alert webhook action ${webhook.webhookActionId}:`,
                        error
                    );
                    throw error;
                }
            }

            // Perform all database updates in a single transaction
            console.log("\nUpdating database in transaction...");
            await db.transaction(async (trx) => {
                // Update idpOidcConfig entries
                for (const update of idpUpdates) {
                    await trx
                        .update(idpOidcConfig)
                        .set({
                            clientId: update.encryptedClientId,
                            clientSecret: update.encryptedClientSecret
                        })
                        .where(
                            eq(
                                idpOidcConfig.idpOauthConfigId,
                                update.idpOauthConfigId
                            )
                        );
                }

                // Update licenseKey entries (delete old, insert new)
                for (const update of licenseKeyUpdates) {
                    // Delete old entry
                    await trx
                        .delete(licenseKey)
                        .where(eq(licenseKey.licenseKeyId, update.oldLicenseKeyId));

                    // Insert new entry with re-encrypted values
                    await trx.insert(licenseKey).values({
                        licenseKeyId: update.newLicenseKeyId,
                        token: update.encryptedToken,
                        instanceId: update.encryptedInstanceId
                    });
                }

                // Update certificate entries
                for (const update of certUpdates) {
                    await trx
                        .update(certificates)
                        .set({
                            certFile: update.encryptedCertFile,
                            keyFile: update.encryptedKeyFile
                        })
                        .where(eq(certificates.certId, update.certId));
                }

                // Update event streaming destination entries
                for (const update of streamingDestinationUpdates) {
                    await trx
                        .update(eventStreamingDestinations)
                        .set({ config: update.encryptedConfig })
                        .where(
                            eq(
                                eventStreamingDestinations.destinationId,
                                update.destinationId
                            )
                        );
                }

                // Update alert webhook action entries
                for (const update of webhookActionUpdates) {
                    await trx
                        .update(alertWebhookActions)
                        .set({ config: update.encryptedConfig })
                        .where(
                            eq(
                                alertWebhookActions.webhookActionId,
                                update.webhookActionId
                            )
                        );
                }
            });

            console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`);
            console.log(`Rotated ${licenseKeyUpdates.length} license key(s)`);
            console.log(`Rotated ${certUpdates.length} certificate(s)`);
            console.log(`Rotated ${streamingDestinationUpdates.length} event streaming destination(s)`);
            console.log(`Rotated ${webhookActionUpdates.length} alert webhook action(s)`);

            // Update config file with new secret
            console.log("\nUpdating config file...");
            config.server.secret = newSecret;
            const newConfigContent = yaml.dump(config, {
                indent: 2,
                lineWidth: -1
            });
            fs.writeFileSync(configPath, newConfigContent, "utf8");

            console.log(`Updated config file: ${configPath}`);

            console.log("\nServer secret rotation completed successfully!");
            console.log(`\nSummary:`);
            console.log(`  - OIDC IdP configurations: ${idpUpdates.length}`);
            console.log(`  - License keys: ${licenseKeyUpdates.length}`);
            console.log(`  - Certificates: ${certUpdates.length}`);
            console.log(`  - Event streaming destinations: ${streamingDestinationUpdates.length}`);
            console.log(`  - Alert webhook actions: ${webhookActionUpdates.length}`);
            console.log(
                `\n  IMPORTANT: Restart the server for the new secret to take effect.`
            );

            process.exit(0);
        } catch (error) {
            console.error("Error rotating server secret:", error);
            process.exit(1);
        }
    }
};

