import crypto from "crypto";
import { promises as fs } from "fs";
import path from "path";
import { databaseLogger } from "./logger.js";

class SystemCrypto {
  private static instance: SystemCrypto;
  private jwtSecret: string | null = null;
  private databaseKey: Buffer | null = null;
  private encryptionKey: Buffer | null = null;
  private internalAuthToken: string | null = null;

  private constructor() {}

  static getInstance(): SystemCrypto {
    if (!this.instance) {
      this.instance = new SystemCrypto();
    }
    return this.instance;
  }

  async initializeJWTSecret(): Promise<void> {
    try {
      const envSecret = process.env.JWT_SECRET;
      if (envSecret && envSecret.length >= 64) {
        this.jwtSecret = envSecret;
        return;
      }

      const dataDir = process.env.DATA_DIR || "./db/data";
      const envPath = path.join(dataDir, ".env");

      try {
        const envContent = await fs.readFile(envPath, "utf8");
        const jwtMatch = envContent.match(/^JWT_SECRET=(.+)$/m);
        if (jwtMatch && jwtMatch[1] && jwtMatch[1].length >= 64) {
          this.jwtSecret = jwtMatch[1];
          process.env.JWT_SECRET = jwtMatch[1];
          databaseLogger.success("JWT secret loaded from .env file", {
            operation: "jwt_init_from_file_success",
            secretLength: jwtMatch[1].length,
            secretPrefix: jwtMatch[1].substring(0, 8) + "...",
          });
          return;
        } else {
          databaseLogger.warn(
            "JWT_SECRET in .env file is invalid or too short",
            {
              operation: "jwt_init_invalid_secret",
              hasMatch: !!jwtMatch,
              secretLength: jwtMatch?.[1]?.length || 0,
            },
          );
        }
      } catch {
        // expected - env file may not exist
      }

      await this.generateAndGuideUser();
    } catch (error) {
      databaseLogger.error("Failed to initialize JWT secret", error, {
        operation: "jwt_init_failed",
      });
      throw new Error("JWT secret initialization failed", { cause: error });
    }
  }

  async getJWTSecret(): Promise<string> {
    if (!this.jwtSecret) {
      await this.initializeJWTSecret();
    }
    return this.jwtSecret!;
  }

  async initializeDatabaseKey(): Promise<void> {
    try {
      const dataDir = process.env.DATA_DIR || "./db/data";
      const envPath = path.join(dataDir, ".env");

      const envKey = process.env.DATABASE_KEY;
      if (envKey && envKey.length >= 64) {
        this.databaseKey = Buffer.from(envKey, "hex");
        return;
      }

      try {
        const envContent = await fs.readFile(envPath, "utf8");
        const dbKeyMatch = envContent.match(/^DATABASE_KEY=(.+)$/m);
        if (dbKeyMatch && dbKeyMatch[1] && dbKeyMatch[1].length >= 64) {
          this.databaseKey = Buffer.from(dbKeyMatch[1], "hex");
          process.env.DATABASE_KEY = dbKeyMatch[1];
          return;
        } else {
          // expected - key not found or invalid length in env file
        }
      } catch {
        // expected - env file may not exist
      }

      await this.generateAndGuideDatabaseKey();
    } catch (error) {
      databaseLogger.error("Failed to initialize database key", error, {
        operation: "db_key_init_failed",
        dataDir: process.env.DATA_DIR || "./db/data",
      });
      throw new Error("Database key initialization failed", { cause: error });
    }
  }

  async getDatabaseKey(): Promise<Buffer> {
    if (!this.databaseKey) {
      await this.initializeDatabaseKey();
    }
    return this.databaseKey!;
  }

  async initializeEncryptionKey(): Promise<void> {
    try {
      const dataDir = process.env.DATA_DIR || "./db/data";
      const envPath = path.join(dataDir, ".env");

      const envKey = process.env.ENCRYPTION_KEY;
      if (envKey && envKey.length >= 64) {
        this.encryptionKey = Buffer.from(envKey, "hex");
        return;
      }

      try {
        const envContent = await fs.readFile(envPath, "utf8");
        const keyMatch = envContent.match(/^ENCRYPTION_KEY=(.+)$/m);
        if (keyMatch && keyMatch[1] && keyMatch[1].length >= 64) {
          this.encryptionKey = Buffer.from(keyMatch[1], "hex");
          process.env.ENCRYPTION_KEY = keyMatch[1];
          return;
        }
      } catch {
        // expected - env file may not exist
      }

      await this.generateAndGuideEncryptionKey();
    } catch (error) {
      databaseLogger.error("Failed to initialize encryption key", error, {
        operation: "encryption_key_init_failed",
        dataDir: process.env.DATA_DIR || "./db/data",
      });
      throw new Error("Encryption key initialization failed", { cause: error });
    }
  }

  async getEncryptionKey(): Promise<Buffer> {
    if (!this.encryptionKey) {
      await this.initializeEncryptionKey();
    }
    return this.encryptionKey!;
  }

  async initializeInternalAuthToken(): Promise<void> {
    try {
      const envToken = process.env.INTERNAL_AUTH_TOKEN;
      if (envToken && envToken.length >= 32) {
        this.internalAuthToken = envToken;
        return;
      }

      const dataDir = process.env.DATA_DIR || "./db/data";
      const envPath = path.join(dataDir, ".env");

      try {
        const envContent = await fs.readFile(envPath, "utf8");
        const tokenMatch = envContent.match(/^INTERNAL_AUTH_TOKEN=(.+)$/m);
        if (tokenMatch && tokenMatch[1] && tokenMatch[1].length >= 32) {
          this.internalAuthToken = tokenMatch[1];
          process.env.INTERNAL_AUTH_TOKEN = tokenMatch[1];
          return;
        }
      } catch {
        // expected - env file may not exist
      }

      await this.generateAndGuideInternalAuthToken();
    } catch (error) {
      databaseLogger.error("Failed to initialize internal auth token", error, {
        operation: "internal_auth_init_failed",
      });
      throw new Error("Internal auth token initialization failed", {
        cause: error,
      });
    }
  }

  async getInternalAuthToken(): Promise<string> {
    if (!this.internalAuthToken) {
      await this.initializeInternalAuthToken();
    }
    return this.internalAuthToken!;
  }

  private async generateAndGuideUser(): Promise<void> {
    const newSecret = crypto.randomBytes(32).toString("hex");
    const instanceId = crypto.randomBytes(8).toString("hex");

    this.jwtSecret = newSecret;

    await this.updateEnvFile("JWT_SECRET", newSecret);

    databaseLogger.success("JWT secret auto-generated and saved to .env", {
      operation: "jwt_auto_generated",
      instanceId,
      envVarName: "JWT_SECRET",
      note: "Ready for use - no restart required",
    });
  }

  private async generateAndGuideDatabaseKey(): Promise<void> {
    const newKey = crypto.randomBytes(32);
    const newKeyHex = newKey.toString("hex");
    const instanceId = crypto.randomBytes(8).toString("hex");

    this.databaseKey = newKey;

    await this.updateEnvFile("DATABASE_KEY", newKeyHex);

    databaseLogger.success("Database key auto-generated and saved to .env", {
      operation: "db_key_auto_generated",
      instanceId,
      envVarName: "DATABASE_KEY",
      note: "Ready for use - no restart required",
    });
  }

  private async generateAndGuideEncryptionKey(): Promise<void> {
    const newKey = crypto.randomBytes(32);
    const newKeyHex = newKey.toString("hex");
    const instanceId = crypto.randomBytes(8).toString("hex");

    this.encryptionKey = newKey;

    await this.updateEnvFile("ENCRYPTION_KEY", newKeyHex);

    databaseLogger.success("Encryption key auto-generated and saved to .env", {
      operation: "encryption_key_auto_generated",
      instanceId,
      envVarName: "ENCRYPTION_KEY",
      note: "Used to wrap session data keys - no restart required",
    });
  }

  private async generateAndGuideInternalAuthToken(): Promise<void> {
    const newToken = crypto.randomBytes(32).toString("hex");
    const instanceId = crypto.randomBytes(8).toString("hex");

    this.internalAuthToken = newToken;

    await this.updateEnvFile("INTERNAL_AUTH_TOKEN", newToken);

    databaseLogger.success(
      "Internal auth token auto-generated and saved to .env",
      {
        operation: "internal_auth_auto_generated",
        instanceId,
        envVarName: "INTERNAL_AUTH_TOKEN",
        note: "Ready for use - no restart required",
      },
    );
  }

  async validateJWTSecret(): Promise<boolean> {
    try {
      const secret = await this.getJWTSecret();
      if (!secret || secret.length < 32) {
        return false;
      }

      const jwt = await import("jsonwebtoken");
      const testPayload = { test: true, timestamp: Date.now() };
      const token = jwt.default.sign(testPayload, secret, { expiresIn: "1s" });
      const decoded = jwt.default.verify(token, secret);

      return !!decoded;
    } catch (error) {
      databaseLogger.error("JWT secret validation failed", error, {
        operation: "jwt_validation_failed",
      });
      return false;
    }
  }

  async getSystemKeyStatus() {
    const isValid = await this.validateJWTSecret();
    const hasSecret = this.jwtSecret !== null;

    const hasEnvVar = !!(
      process.env.JWT_SECRET && process.env.JWT_SECRET.length >= 64
    );

    return {
      hasSecret,
      isValid,
      storage: {
        environment: hasEnvVar,
      },
      algorithm: "HS256",
      note: "Using simplified key management without encryption layers",
    };
  }

  private async updateEnvFile(key: string, value: string): Promise<void> {
    const dataDir = process.env.DATA_DIR || "./db/data";
    const envPath = path.join(dataDir, ".env");

    try {
      await fs.mkdir(dataDir, { recursive: true });

      let envContent = "";

      try {
        envContent = await fs.readFile(envPath, "utf8");
      } catch {
        envContent = "# Termix Auto-generated Configuration\n\n";
      }

      const keyRegex = new RegExp(`^${key}=.*$`, "m");

      if (keyRegex.test(envContent)) {
        envContent = envContent.replace(keyRegex, `${key}=${value}`);
      } else {
        if (!envContent.includes("# Security Keys")) {
          envContent += "\n# Security Keys (Auto-generated)\n";
        }
        envContent += `${key}=${value}\n`;
      }

      await fs.writeFile(envPath, envContent);

      process.env[key] = value;
    } catch (error) {
      databaseLogger.error(`Failed to update .env file with ${key}`, error, {
        operation: "env_file_update_failed",
        key,
      });
      throw error;
    }
  }
}

export { SystemCrypto };
