import { getErrorMessage } from "./utils/error-message.js";
import dotenv from "dotenv";
import { promises as fs, readFileSync } from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { AutoSSLSetup } from "./utils/auto-ssl-setup.js";
import { AuthManager } from "./utils/auth-manager.js";
import { DataCrypto } from "./utils/data-crypto.js";
import { ensureDatabaseLayerPreupgradeBackup } from "./utils/database-layer-preupgrade-backup.js";
import { DatabaseSaveTrigger } from "./utils/database-save-trigger.js";
import { SystemCrypto } from "./utils/system-crypto.js";
import {
  systemLogger,
  versionLogger,
  setGlobalLogLevel,
} from "./utils/logger.js";
import { getTrustedProxyAuthConfig } from "./utils/trusted-proxy-auth.js";

/**
 * host:port from DATABASE_URL for the startup log. Parsed rather than printed
 * so the password the URL also carries never reaches the logs.
 */
function describeDatabaseHost(): string {
  const raw = process.env.DATABASE_URL?.trim();
  if (!raw) return "unknown";

  try {
    const { host } = new URL(raw);
    return host || "unknown";
  } catch {
    return "unknown";
  }
}

async function provisionLocalDesktopUserIfNeeded(): Promise<void> {
  const { createCurrentUserRepository, createCurrentRoleRepository } =
    await import("./database/repositories/factory.js");
  const { AuthManager } = await import("./utils/auth-manager.js");
  const crypto = await import("crypto");

  const userRepository = createCurrentUserRepository();
  const existingCount = await userRepository.countAll();
  if (existingCount > 0) {
    const allUsers = await userRepository.listAll();
    for (const user of allUsers) {
      try {
        await AuthManager.getInstance().registerUser(user.id);
      } catch (dekError) {
        systemLogger.error(
          "Failed to verify/provision data-encryption key for existing user",
          dekError,
          { operation: "desktop_dek_healing", userId: user.id },
        );
      }
    }
    return;
  }

  const id = crypto.randomUUID();
  const { isFirstUser } = await userRepository.createFirstLocalUser({
    id,
    username: "local",
    passwordHash: "",
    isOidc: false,
    clientId: "",
    clientSecret: "",
    issuerUrl: "",
    authorizationUrl: "",
    tokenUrl: "",
    identifierPath: "",
    namePath: "",
    scopes: "openid email profile",
    totpSecret: null,
    totpEnabled: false,
    totpBackupCodes: null,
  });

  try {
    await createCurrentRoleRepository().assignRoleNameToUser({
      userId: id,
      roleName: isFirstUser ? "admin" : "user",
      grantedBy: id,
    });
  } catch (roleError) {
    systemLogger.error(
      "Failed to assign default role to auto-provisioned local user",
      roleError,
      { operation: "desktop_auto_provision_role" },
    );
  }

  await AuthManager.getInstance().registerUser(
    id,
    crypto.randomBytes(32).toString("hex"),
  );

  systemLogger.success("Auto-provisioned local desktop user", {
    operation: "desktop_auto_provision",
    userId: id,
  });
}

(async () => {
  const initStartTime = Date.now();
  try {
    dotenv.config({ quiet: true });

    const dataDir = process.env.DATA_DIR || "./db/data";
    const envPath = path.join(dataDir, ".env");
    try {
      await fs.access(envPath);
      const persistentConfig = dotenv.config({ path: envPath, quiet: true });
      if (persistentConfig.parsed) {
        Object.assign(process.env, persistentConfig.parsed);
      }
    } catch {
      // expected - env file may not exist
    }

    systemLogger.info("Termix backend initialization started", {
      operation: "backend_init_start",
      nodeEnv: process.env.NODE_ENV || "production",
      port: process.env.PORT || 4090,
    });

    let version = process.env.VERSION || "unknown";
    if (version === "unknown") {
      const candidates = [
        path.join(process.cwd(), "package.json"),
        path.join(
          path.dirname(fileURLToPath(import.meta.url)),
          "../../../package.json",
        ),
      ];
      for (const packageJsonPath of candidates) {
        try {
          const packageJson = JSON.parse(
            readFileSync(packageJsonPath, "utf-8"),
          );
          if (packageJson.version) {
            version = packageJson.version;
            break;
          }
        } catch {
          // try the next location
        }
      }
    }
    process.env.VERSION = version;

    versionLogger.info(`Termix Backend starting - Version: ${version}`, {
      operation: "startup",
      version: version,
    });

    const trustedProxyAuth = getTrustedProxyAuthConfig();

    const systemCrypto = SystemCrypto.getInstance();
    await systemCrypto.initializeJWTSecret();
    await systemCrypto.initializeDatabaseKey();
    await systemCrypto.initializeEncryptionKey();
    await systemCrypto.initializeInternalAuthToken();

    const { needsExplicitPersist, resolveDatabaseDialect } =
      await import("./database/db/dialect.js");
    const databaseDialect = resolveDatabaseDialect();

    // The pre-upgrade backup copies the SQLite file, so there is nothing for it
    // to do on a client-server engine. Say so rather than no-op silently:
    // backups are the operator's own responsibility there.
    if (needsExplicitPersist(databaseDialect)) {
      ensureDatabaseLayerPreupgradeBackup({ dataDir, version });
    } else {
      systemLogger.info(
        `Skipping pre-upgrade backup on ${databaseDialect} - back up the database yourself`,
        {
          operation: "backend_init_db_backup_skipped",
          dialect: databaseDialect,
        },
      );
    }

    await AutoSSLSetup.initialize();
    systemLogger.success("SSL setup completed", {
      operation: "backend_init_ssl",
      sslEnabled: process.env.ENABLE_SSL === "true",
    });

    const dbModule = await import("./database/db/index.js");
    await dbModule.initializeDatabase();
    // Naming the engine makes a misconfiguration obvious: without it, a bad
    // DATABASE_DIALECT silently falls back to SQLite and looks like data loss.
    systemLogger.success(`Database initialized (${databaseDialect})`, {
      operation: "backend_init_db",
      dialect: databaseDialect,
      // Host only, never the credentials the URL also carries.
      ...(needsExplicitPersist(databaseDialect)
        ? {}
        : { host: describeDatabaseHost() }),
    });

    if (trustedProxyAuth.enabled) {
      const {
        createCurrentSettingsRepository,
        createCurrentSsoProviderRepository,
        createCurrentUserRepository,
      } = await import("./database/repositories/factory.js");
      const [legacyOidc, providers, users] = await Promise.all([
        createCurrentSettingsRepository().get("oidc_config"),
        createCurrentSsoProviderRepository().listEnabled(),
        createCurrentUserRepository().listAll(),
      ]);
      const conflictingProvider = providers.some((provider) =>
        ["oidc", "github", "google"].includes(provider.type),
      );
      const conflictingUser = users.some(
        (user) => user.isOidc || user.totpEnabled,
      );
      if (
        legacyOidc ||
        process.env.OIDC_CLIENT_ID ||
        conflictingProvider ||
        conflictingUser
      ) {
        throw new Error(
          "Trusted proxy authentication cannot start while OIDC or TOTP is enabled",
        );
      }
      systemLogger.info("Trusted proxy authentication enabled", {
        operation: "trusted_proxy_auth_enabled",
        usernameHeader: trustedProxyAuth.usernameHeader,
        roleHeader: trustedProxyAuth.roleHeader,
        trustedProxyCount: trustedProxyAuth.trustedProxies.length,
      });
    }

    const { UserKeyManager } = await import("./utils/user-keys.js");
    await UserKeyManager.getInstance().initialize();

    const { runBootDekMigration } =
      await import("./utils/crypto-migration/dek-migration.js");
    await runBootDekMigration({ cleanupLegacy: true });

    const { runLegacySharedCredentialCleanup } =
      await import("./utils/crypto-migration/legacy-share-cleanup.js");
    await runLegacySharedCredentialCleanup();

    const authManager = AuthManager.getInstance();
    await authManager.initialize();
    DataCrypto.initialize();

    const { runLegacySharedSshAuthOptInMigration } =
      await import("./utils/crypto-migration/legacy-shared-ssh-auth-opt-in-migration.js");
    await runLegacySharedSshAuthOptInMigration();

    const { runSharedHostSecretsMigration } =
      await import("./utils/crypto-migration/shared-host-secrets-migration.js");
    await runSharedHostSecretsMigration();

    const { runPrivateSharedSshAuthMigration } =
      await import("./utils/crypto-migration/private-shared-ssh-auth-migration.js");
    await runPrivateSharedSshAuthMigration();

    const { runChannelConfigEncryptionMigration } =
      await import("./utils/crypto-migration/channel-config-encryption.js");
    await runChannelConfigEncryptionMigration();

    const { runAutomationsMigration } =
      await import("./utils/crypto-migration/automations-migration.js");
    await runAutomationsMigration();

    if (process.env.ELECTRON_EMBEDDED === "true") {
      await provisionLocalDesktopUserIfNeeded();
    }

    import("./utils/opkssh-binary-manager.js").then(
      ({ OPKSSHBinaryManager }) => {
        OPKSSHBinaryManager.ensureBinary().catch((error) => {
          const dataDir =
            process.env.DATA_DIR || path.join(process.cwd(), "db", "data");
          systemLogger.warn(
            "Failed to initialize OPKSSH binary - OPKSSH authentication will not be available",
            {
              operation: "opkssh_binary_init_failed",
              error: getErrorMessage(error),
              stack: error instanceof Error ? error.stack : undefined,
              platform: process.platform,
              arch: process.arch,
              dataDir,
            },
          );
        });
      },
    );

    const { serverReady } = await import("./database/database.js");
    await serverReady;
    await import("./hosts/terminal/index.js");
    await import("./hosts/tunnel/index.js");
    await import("./hosts/file-manager/index.js");
    await import("./hosts/metrics/index.js");
    await import("./hosts/docker/index.js");
    await import("./hosts/docker/console.js");
    await import("./hosts/tmux/index.js");
    await import("./hosts/serial.js");
    await import("./services/dashboard.js");
    await import("./services/homepage.js");

    // Initialize log level from database settings
    const { getCurrentSettingValue } =
      await import("./database/repositories/factory.js");
    const logLevel = getCurrentSettingValue("log_level");
    if (logLevel) {
      setGlobalLogLevel(logLevel);
      systemLogger.info(`Log level set to: ${logLevel}`, {
        operation: "log_level_init",
      });
    }

    // Initialize Guacamole server for RDP/VNC/Telnet support
    const guacEnabled = getCurrentSettingValue("guac_enabled") !== "false";

    if (process.env.ENABLE_GUACAMOLE !== "false" && guacEnabled) {
      import("./hosts/guacamole/guacamole-server.js")
        .then(() => {
          systemLogger.info("Guacamole server initialized", {
            operation: "guac_init",
          });
        })
        .catch((error) => {
          systemLogger.warn(
            "Failed to initialize Guacamole server (guacd may not be available)",
            {
              operation: "guac_init_skip",
              error: getErrorMessage(error),
            },
          );
        });
    }

    // After metrics, which the automation triggers and headless polling hook into.
    const { startAutomationScheduler } =
      await import("./automations/scheduler.js");
    startAutomationScheduler();

    const { startAnalyticsHeartbeat } = await import("./utils/analytics.js");
    startAnalyticsHeartbeat();

    systemLogger.success("Termix backend started successfully", {
      operation: "backend_init_complete",
      port: process.env.PORT || 4090,
      ssl: process.env.ENABLE_SSL === "true",
      duration: Date.now() - initStartTime,
    });

    const gracefulShutdown = async (signal: string) => {
      systemLogger.info(`Received ${signal}, initiating graceful shutdown...`, {
        operation: "shutdown",
      });
      // Only SQLite has anything to flush. On a client-server engine the writes
      // committed as they happened, so there is no file to save and claiming
      // otherwise in the log would be untrue.
      if (needsExplicitPersist(databaseDialect)) {
        try {
          await DatabaseSaveTrigger.forceSave("shutdown_explicit_save");
          systemLogger.info("Database saved to disk before exit", {
            operation: "shutdown_db_saved",
          });
        } catch (error) {
          systemLogger.error("Failed to save database during shutdown", error, {
            operation: "shutdown_db_save_failed",
          });
        }
      }
      process.exit(0);
    };

    process.on("SIGINT", () => gracefulShutdown("SIGINT"));
    process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));

    process.on("message", (msg: { type?: string }) => {
      if (msg?.type === "shutdown") {
        gracefulShutdown("IPC shutdown");
      }
    });

    // A single bad request must not take the server down. Exit only on errors
    // that leave the process genuinely unusable; log and keep serving
    // otherwise, since these are almost always scoped to one connection.
    const isFatalError = (error: unknown): boolean => {
      const code = (error as NodeJS.ErrnoException)?.code;
      if (code === "ERR_WORKER_OUT_OF_MEMORY") return true;
      if (error instanceof RangeError) {
        return /call stack|heap out of memory/i.test(error.message);
      }
      return false;
    };

    process.on("uncaughtException", (error) => {
      systemLogger.error("Uncaught exception occurred", error, {
        operation: "error_handling",
        fatal: isFatalError(error),
      });
      if (isFatalError(error)) {
        process.exit(1);
      }
    });

    process.on("unhandledRejection", (reason) => {
      systemLogger.error("Unhandled promise rejection", reason, {
        operation: "error_handling",
        fatal: isFatalError(reason),
      });
      if (isFatalError(reason)) {
        process.exit(1);
      }
    });
  } catch (error) {
    systemLogger.error("Failed to initialize backend services", error, {
      operation: "startup_failed",
    });
    process.exit(1);
  }
})();
