import crypto from "crypto";
import { getDb } from "../database/db/index.js";
import { settings } from "../database/db/schema.js";
import { eq } from "drizzle-orm";
import { databaseLogger } from "./logger.js";

interface KEKSalt {
  salt: string;
  iterations: number;
  algorithm: string;
  createdAt: string;
}

interface EncryptedDEK {
  data: string;
  iv: string;
  tag: string;
  algorithm: string;
  createdAt: string;
}

interface UserSession {
  dataKey: Buffer;
  expiresAt: number;
  lastActivity?: number;
}

class UserCrypto {
  private static instance: UserCrypto;
  private userSessions: Map<string, UserSession> = new Map();
  private sessionExpiredCallback?: (userId: string) => void;

  private static readonly PBKDF2_ITERATIONS = 100000;
  private static readonly KEK_LENGTH = 32;
  private static readonly DEK_LENGTH = 32;

  private constructor() {
    setInterval(
      () => {
        this.cleanupExpiredSessions();
      },
      5 * 60 * 1000,
    );
  }

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

  setSessionExpiredCallback(callback: (userId: string) => void): void {
    this.sessionExpiredCallback = callback;
  }

  async setupUserEncryption(userId: string, password: string): Promise<void> {
    const kekSalt = await this.generateKEKSalt();
    await this.storeKEKSalt(userId, kekSalt);

    const KEK = this.deriveKEK(password, kekSalt);
    const DEK = crypto.randomBytes(UserCrypto.DEK_LENGTH);
    const encryptedDEK = this.encryptDEK(DEK, KEK);
    await this.storeEncryptedDEK(userId, encryptedDEK);

    KEK.fill(0);
    DEK.fill(0);
  }

  async setupOIDCUserEncryption(
    userId: string,
    sessionDurationMs: number,
  ): Promise<void> {
    const existingEncryptedDEK = await this.getEncryptedDEK(userId);

    let DEK: Buffer;

    if (existingEncryptedDEK) {
      const systemKey = this.deriveOIDCSystemKey(userId);
      DEK = this.decryptDEK(existingEncryptedDEK, systemKey);
      systemKey.fill(0);
    } else {
      DEK = crypto.randomBytes(UserCrypto.DEK_LENGTH);
      const systemKey = this.deriveOIDCSystemKey(userId);

      try {
        const encryptedDEK = this.encryptDEK(DEK, systemKey);
        await this.storeEncryptedDEK(userId, encryptedDEK);

        const storedEncryptedDEK = await this.getEncryptedDEK(userId);
        if (
          storedEncryptedDEK &&
          storedEncryptedDEK.data !== encryptedDEK.data
        ) {
          DEK.fill(0);
          DEK = this.decryptDEK(storedEncryptedDEK, systemKey);
        } else if (!storedEncryptedDEK) {
          throw new Error("Failed to store and retrieve user encryption key.");
        }
      } finally {
        systemKey.fill(0);
      }
    }

    const now = Date.now();
    this.userSessions.set(userId, {
      dataKey: Buffer.from(DEK),
      expiresAt: now + sessionDurationMs,
    });

    DEK.fill(0);
  }

  async authenticateUser(
    userId: string,
    password: string,
    sessionDurationMs: number,
  ): Promise<boolean> {
    try {
      const kekSalt = await this.getKEKSalt(userId);
      if (!kekSalt) return false;

      const KEK = this.deriveKEK(password, kekSalt);
      const encryptedDEK = await this.getEncryptedDEK(userId);
      if (!encryptedDEK) {
        KEK.fill(0);
        return false;
      }

      const DEK = this.decryptDEK(encryptedDEK, KEK);
      KEK.fill(0);

      if (!DEK || DEK.length === 0) {
        databaseLogger.error("DEK is empty or invalid after decryption", {
          operation: "user_crypto_auth_debug",
          userId,
          dekLength: DEK ? DEK.length : 0,
        });
        return false;
      }

      const now = Date.now();

      const oldSession = this.userSessions.get(userId);
      if (oldSession) {
        oldSession.dataKey.fill(0);
      }

      this.userSessions.set(userId, {
        dataKey: Buffer.from(DEK),
        expiresAt: now + sessionDurationMs,
      });

      DEK.fill(0);

      return true;
    } catch (error) {
      databaseLogger.warn("User authentication failed", {
        operation: "user_crypto_auth_failed",
        userId,
        error: error instanceof Error ? error.message : "Unknown",
      });
      return false;
    }
  }

  async authenticateOIDCUser(
    userId: string,
    sessionDurationMs: number,
  ): Promise<boolean> {
    try {
      const oidcEncryptedDEK = await this.getOIDCEncryptedDEK(userId);

      if (oidcEncryptedDEK) {
        const systemKey = this.deriveOIDCSystemKey(userId);
        const DEK = this.decryptDEK(oidcEncryptedDEK, systemKey);
        systemKey.fill(0);

        if (!DEK || DEK.length === 0) {
          databaseLogger.error(
            "Failed to decrypt OIDC DEK for dual-auth user",
            {
              operation: "oidc_auth_dual_decrypt_failed",
              userId,
            },
          );
          return false;
        }

        const now = Date.now();
        const oldSession = this.userSessions.get(userId);
        if (oldSession) {
          oldSession.dataKey.fill(0);
        }

        this.userSessions.set(userId, {
          dataKey: Buffer.from(DEK),
          expiresAt: now + sessionDurationMs,
        });

        DEK.fill(0);
        return true;
      }

      const kekSalt = await this.getKEKSalt(userId);
      const encryptedDEK = await this.getEncryptedDEK(userId);

      if (!kekSalt || !encryptedDEK) {
        await this.setupOIDCUserEncryption(userId, sessionDurationMs);
        return true;
      }

      const systemKey = this.deriveOIDCSystemKey(userId);
      const DEK = this.decryptDEK(encryptedDEK, systemKey);
      systemKey.fill(0);

      if (!DEK || DEK.length === 0) {
        await this.setupOIDCUserEncryption(userId, sessionDurationMs);
        return true;
      }

      const now = Date.now();

      const oldSession = this.userSessions.get(userId);
      if (oldSession) {
        oldSession.dataKey.fill(0);
      }

      this.userSessions.set(userId, {
        dataKey: Buffer.from(DEK),
        expiresAt: now + sessionDurationMs,
      });

      DEK.fill(0);

      return true;
    } catch (error) {
      databaseLogger.error("OIDC authentication failed", error, {
        operation: "oidc_auth_error",
        userId,
        error: error instanceof Error ? error.message : "Unknown",
      });
      await this.setupOIDCUserEncryption(userId, sessionDurationMs);
      return true;
    }
  }

  getUserDataKey(userId: string): Buffer | null {
    const session = this.userSessions.get(userId);
    if (!session) {
      return null;
    }

    const now = Date.now();

    if (now > session.expiresAt) {
      this.userSessions.delete(userId);
      session.dataKey.fill(0);
      if (this.sessionExpiredCallback) {
        this.sessionExpiredCallback(userId);
      }
      return null;
    }

    return session.dataKey;
  }

  logoutUser(userId: string): void {
    const session = this.userSessions.get(userId);
    if (session) {
      session.dataKey.fill(0);
      this.userSessions.delete(userId);
    }
  }

  isUserUnlocked(userId: string): boolean {
    return this.getUserDataKey(userId) !== null;
  }

  async changeUserPassword(
    userId: string,
    oldPassword: string,
    newPassword: string,
  ): Promise<boolean> {
    try {
      const isValid = await this.validatePassword(userId, oldPassword);
      if (!isValid) return false;

      const kekSalt = await this.getKEKSalt(userId);
      if (!kekSalt) return false;

      const oldKEK = this.deriveKEK(oldPassword, kekSalt);
      const encryptedDEK = await this.getEncryptedDEK(userId);
      if (!encryptedDEK) return false;

      const DEK = this.decryptDEK(encryptedDEK, oldKEK);

      const newKekSalt = await this.generateKEKSalt();
      const newKEK = this.deriveKEK(newPassword, newKekSalt);

      const newEncryptedDEK = this.encryptDEK(DEK, newKEK);

      await this.storeKEKSalt(userId, newKekSalt);
      await this.storeEncryptedDEK(userId, newEncryptedDEK);

      const { saveMemoryDatabaseToFile } = await import(
        "../database/db/index.js"
      );
      await saveMemoryDatabaseToFile();

      oldKEK.fill(0);
      newKEK.fill(0);
      DEK.fill(0);

      return true;
    } catch (error) {
      databaseLogger.error("Password change failed", error, {
        operation: "password_change_error",
        userId,
        error: error instanceof Error ? error.message : "Unknown error",
      });
      return false;
    }
  }

  async resetUserPasswordWithPreservedDEK(
    userId: string,
    newPassword: string,
  ): Promise<boolean> {
    try {
      const existingDEK = this.getUserDataKey(userId);
      if (!existingDEK) {
        return false;
      }

      const newKekSalt = await this.generateKEKSalt();
      const newKEK = this.deriveKEK(newPassword, newKekSalt);

      const newEncryptedDEK = this.encryptDEK(existingDEK, newKEK);

      await this.storeKEKSalt(userId, newKekSalt);
      await this.storeEncryptedDEK(userId, newEncryptedDEK);

      const { saveMemoryDatabaseToFile } = await import(
        "../database/db/index.js"
      );
      await saveMemoryDatabaseToFile();

      newKEK.fill(0);

      const session = this.userSessions.get(userId);
      if (session) {
        session.lastActivity = Date.now();
      }

      return true;
    } catch (error) {
      databaseLogger.error("Password reset with preserved DEK failed", error, {
        operation: "password_reset_preserve_error",
        userId,
        error: error instanceof Error ? error.message : "Unknown error",
      });
      return false;
    }
  }

  /**
   * Convert a password-based user's encryption to DUAL-AUTH encryption.
   * This is used when linking an OIDC account to a password account for dual-auth.
   *
   * IMPORTANT: This does NOT delete the password-based KEK salt!
   * The user needs to maintain BOTH password and OIDC authentication methods.
   * We keep the password KEK salt so password login still works.
   * We also store the DEK encrypted with OIDC system key for OIDC login.
   */
  async convertToOIDCEncryption(userId: string): Promise<void> {
    try {
      const existingEncryptedDEK = await this.getEncryptedDEK(userId);
      const existingKEKSalt = await this.getKEKSalt(userId);

      if (!existingEncryptedDEK && !existingKEKSalt) {
        databaseLogger.warn("No existing encryption to convert for user", {
          operation: "convert_to_oidc_encryption_skip",
          userId,
        });
        return;
      }

      const existingDEK = this.getUserDataKey(userId);

      if (!existingDEK) {
        throw new Error(
          "Cannot convert to OIDC encryption - user session not active. Please log in with password first.",
        );
      }

      const systemKey = this.deriveOIDCSystemKey(userId);
      const oidcEncryptedDEK = this.encryptDEK(existingDEK, systemKey);
      systemKey.fill(0);

      const key = `user_encrypted_dek_oidc_${userId}`;
      const value = JSON.stringify(oidcEncryptedDEK);

      const { getDb } = await import("../database/db/index.js");
      const { settings } = await import("../database/db/schema.js");
      const { eq } = await import("drizzle-orm");

      const existing = await getDb()
        .select()
        .from(settings)
        .where(eq(settings.key, key));

      if (existing.length > 0) {
        await getDb()
          .update(settings)
          .set({ value })
          .where(eq(settings.key, key));
      } else {
        await getDb().insert(settings).values({ key, value });
      }

      databaseLogger.info(
        "Converted user encryption to dual-auth (password + OIDC)",
        {
          operation: "convert_to_oidc_encryption_preserved",
          userId,
        },
      );

      const { saveMemoryDatabaseToFile } = await import(
        "../database/db/index.js"
      );
      await saveMemoryDatabaseToFile();
    } catch (error) {
      databaseLogger.error("Failed to convert to OIDC encryption", error, {
        operation: "convert_to_oidc_encryption_error",
        userId,
        error: error instanceof Error ? error.message : "Unknown error",
      });
      throw error;
    }
  }

  private async validatePassword(
    userId: string,
    password: string,
  ): Promise<boolean> {
    try {
      const kekSalt = await this.getKEKSalt(userId);
      if (!kekSalt) return false;

      const KEK = this.deriveKEK(password, kekSalt);
      const encryptedDEK = await this.getEncryptedDEK(userId);
      if (!encryptedDEK) return false;

      const DEK = this.decryptDEK(encryptedDEK, KEK);

      KEK.fill(0);
      DEK.fill(0);

      return true;
    } catch {
      return false;
    }
  }

  private cleanupExpiredSessions(): void {
    const now = Date.now();
    const expiredUsers: string[] = [];

    for (const [userId, session] of this.userSessions.entries()) {
      if (now > session.expiresAt) {
        session.dataKey.fill(0);
        expiredUsers.push(userId);
      }
    }

    expiredUsers.forEach((userId) => {
      this.userSessions.delete(userId);
    });
  }

  private async generateKEKSalt(): Promise<KEKSalt> {
    return {
      salt: crypto.randomBytes(32).toString("hex"),
      iterations: UserCrypto.PBKDF2_ITERATIONS,
      algorithm: "pbkdf2-sha256",
      createdAt: new Date().toISOString(),
    };
  }

  private deriveKEK(password: string, kekSalt: KEKSalt): Buffer {
    return crypto.pbkdf2Sync(
      password,
      Buffer.from(kekSalt.salt, "hex"),
      kekSalt.iterations,
      UserCrypto.KEK_LENGTH,
      "sha256",
    );
  }

  private deriveOIDCSystemKey(userId: string): Buffer {
    const systemSecret =
      process.env.OIDC_SYSTEM_SECRET || "termix-oidc-system-secret-default";
    const salt = Buffer.from(userId, "utf8");
    return crypto.pbkdf2Sync(
      systemSecret,
      salt,
      100000,
      UserCrypto.KEK_LENGTH,
      "sha256",
    );
  }

  private encryptDEK(dek: Buffer, kek: Buffer): EncryptedDEK {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv("aes-256-gcm", kek, iv);

    let encrypted = cipher.update(dek);
    encrypted = Buffer.concat([encrypted, cipher.final()]);
    const tag = cipher.getAuthTag();

    return {
      data: encrypted.toString("hex"),
      iv: iv.toString("hex"),
      tag: tag.toString("hex"),
      algorithm: "aes-256-gcm",
      createdAt: new Date().toISOString(),
    };
  }

  private decryptDEK(encryptedDEK: EncryptedDEK, kek: Buffer): Buffer {
    const decipher = crypto.createDecipheriv(
      "aes-256-gcm",
      kek,
      Buffer.from(encryptedDEK.iv, "hex"),
    );

    decipher.setAuthTag(Buffer.from(encryptedDEK.tag, "hex"));
    let decrypted = decipher.update(Buffer.from(encryptedDEK.data, "hex"));
    decrypted = Buffer.concat([decrypted, decipher.final()]);

    return decrypted;
  }

  private async storeKEKSalt(userId: string, kekSalt: KEKSalt): Promise<void> {
    const key = `user_kek_salt_${userId}`;
    const value = JSON.stringify(kekSalt);

    const existing = await getDb()
      .select()
      .from(settings)
      .where(eq(settings.key, key));

    if (existing.length > 0) {
      await getDb()
        .update(settings)
        .set({ value })
        .where(eq(settings.key, key));
    } else {
      await getDb().insert(settings).values({ key, value });
    }
  }

  private async getKEKSalt(userId: string): Promise<KEKSalt | null> {
    try {
      const key = `user_kek_salt_${userId}`;
      const result = await getDb()
        .select()
        .from(settings)
        .where(eq(settings.key, key));

      if (result.length === 0) {
        return null;
      }

      return JSON.parse(result[0].value);
    } catch {
      return null;
    }
  }

  private async storeEncryptedDEK(
    userId: string,
    encryptedDEK: EncryptedDEK,
  ): Promise<void> {
    const key = `user_encrypted_dek_${userId}`;
    const value = JSON.stringify(encryptedDEK);

    const existing = await getDb()
      .select()
      .from(settings)
      .where(eq(settings.key, key));

    if (existing.length > 0) {
      await getDb()
        .update(settings)
        .set({ value })
        .where(eq(settings.key, key));
    } else {
      await getDb().insert(settings).values({ key, value });
    }
  }

  private async getEncryptedDEK(userId: string): Promise<EncryptedDEK | null> {
    try {
      const key = `user_encrypted_dek_${userId}`;
      const result = await getDb()
        .select()
        .from(settings)
        .where(eq(settings.key, key));

      if (result.length === 0) {
        return null;
      }

      return JSON.parse(result[0].value);
    } catch {
      return null;
    }
  }

  private async getOIDCEncryptedDEK(
    userId: string,
  ): Promise<EncryptedDEK | null> {
    try {
      const key = `user_encrypted_dek_oidc_${userId}`;
      const result = await getDb()
        .select()
        .from(settings)
        .where(eq(settings.key, key));

      if (result.length === 0) {
        return null;
      }

      return JSON.parse(result[0].value);
    } catch {
      return null;
    }
  }
}

export { UserCrypto, type KEKSalt, type EncryptedDEK };
