import { getErrorMessage } from "./error-message.js";
import crypto from "crypto";
import fs from "fs";
import path from "path";
import { databaseLogger } from "./logger.js";
import { SystemCrypto } from "./system-crypto.js";

interface EncryptedFileMetadata {
  iv: string;
  tag: string;
  version: string;
  fingerprint: string;
  algorithm: string;
  keySource?: string;
  salt?: string;
  dataSize?: number;
}

class DatabaseFileEncryption {
  private static readonly VERSION = "v2";
  private static readonly ALGORITHM = "aes-256-gcm";
  private static readonly ENCRYPTED_FILE_SUFFIX = ".encrypted";
  private static readonly METADATA_FILE_SUFFIX = ".meta";
  private static systemCrypto = SystemCrypto.getInstance();

  static async encryptDatabaseFromBuffer(
    buffer: Buffer,
    targetPath: string,
  ): Promise<string> {
    const tmpPath = `${targetPath}.tmp-${Date.now()}-${process.pid}`;
    const metadataPath = `${targetPath}${this.METADATA_FILE_SUFFIX}`;

    try {
      const key = await this.systemCrypto.getDatabaseKey();
      const iv = crypto.randomBytes(16);
      const cipher = crypto.createCipheriv(
        this.ALGORITHM,
        key,
        iv,
      ) as crypto.CipherGCM;
      const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
      const tag = cipher.getAuthTag();

      const metadata: EncryptedFileMetadata = {
        iv: iv.toString("hex"),
        tag: tag.toString("hex"),
        version: this.VERSION,
        fingerprint: "termix-v2-systemcrypto",
        algorithm: this.ALGORITHM,
        keySource: "SystemCrypto",
        dataSize: encrypted.length,
      };

      const metadataJson = JSON.stringify(metadata, null, 2);
      const metadataBuffer = Buffer.from(metadataJson, "utf8");
      const metadataLengthBuffer = Buffer.alloc(4);
      metadataLengthBuffer.writeUInt32BE(metadataBuffer.length, 0);

      const finalBuffer = Buffer.concat([
        metadataLengthBuffer,
        metadataBuffer,
        encrypted,
      ]);

      fs.writeFileSync(tmpPath, finalBuffer);
      fs.renameSync(tmpPath, targetPath);

      try {
        if (fs.existsSync(metadataPath)) {
          fs.unlinkSync(metadataPath);
        }
      } catch (cleanupError) {
        databaseLogger.warn("Failed to cleanup old metadata file", {
          operation: "old_meta_cleanup_failed",
          path: metadataPath,
          error: getErrorMessage(cleanupError),
        });
      }

      return targetPath;
    } catch (error) {
      try {
        if (fs.existsSync(tmpPath)) {
          fs.unlinkSync(tmpPath);
        }
      } catch (cleanupError) {
        databaseLogger.warn("Failed to cleanup temporary files", {
          operation: "temp_file_cleanup_failed",
          tmpPath,
          error: getErrorMessage(cleanupError),
        });
      }

      databaseLogger.error("Failed to encrypt database buffer", error, {
        operation: "database_buffer_encryption_failed",
        targetPath,
      });
      throw new Error(
        `Database buffer encryption failed: ${getErrorMessage(error)}`,
        { cause: error },
      );
    }
  }

  static async encryptDatabaseFile(
    sourcePath: string,
    targetPath?: string,
  ): Promise<string> {
    if (!fs.existsSync(sourcePath)) {
      throw new Error(`Source database file does not exist: ${sourcePath}`);
    }

    const encryptedPath =
      targetPath || `${sourcePath}${this.ENCRYPTED_FILE_SUFFIX}`;
    const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
    const tmpPath = `${encryptedPath}.tmp-${Date.now()}-${process.pid}`;
    const tmpMetadataPath = `${tmpPath}${this.METADATA_FILE_SUFFIX}`;

    try {
      const sourceData = fs.readFileSync(sourcePath);

      const key = await this.systemCrypto.getDatabaseKey();

      const iv = crypto.randomBytes(16);

      const cipher = crypto.createCipheriv(
        this.ALGORITHM,
        key,
        iv,
      ) as crypto.CipherGCM;
      const encrypted = Buffer.concat([
        cipher.update(sourceData),
        cipher.final(),
      ]);
      const tag = cipher.getAuthTag();

      const keyFingerprint = crypto
        .createHash("sha256")
        .update(key)
        .digest("hex")
        .substring(0, 16);

      const metadata: EncryptedFileMetadata = {
        iv: iv.toString("hex"),
        tag: tag.toString("hex"),
        version: this.VERSION,
        fingerprint: "termix-v2-systemcrypto",
        algorithm: this.ALGORITHM,
        keySource: "SystemCrypto",
        dataSize: encrypted.length,
      };

      fs.writeFileSync(tmpPath, encrypted);
      fs.writeFileSync(tmpMetadataPath, JSON.stringify(metadata, null, 2));

      fs.renameSync(tmpPath, encryptedPath);
      fs.renameSync(tmpMetadataPath, metadataPath);

      databaseLogger.info("Database file encrypted successfully", {
        operation: "database_file_encryption",
        sourcePath,
        encryptedPath,
        fileSize: sourceData.length,
        encryptedSize: encrypted.length,
        keyFingerprint,
        fingerprintPrefix: metadata.fingerprint,
      });

      return encryptedPath;
    } catch (error) {
      try {
        if (fs.existsSync(tmpPath)) {
          fs.unlinkSync(tmpPath);
        }
        if (fs.existsSync(tmpMetadataPath)) {
          fs.unlinkSync(tmpMetadataPath);
        }
      } catch (cleanupError) {
        databaseLogger.warn("Failed to cleanup temporary files", {
          operation: "temp_file_cleanup_failed",
          tmpPath,
          error: getErrorMessage(cleanupError),
        });
      }

      databaseLogger.error("Failed to encrypt database file", error, {
        operation: "database_file_encryption_failed",
        sourcePath,
        targetPath: encryptedPath,
      });
      throw new Error(
        `Database file encryption failed: ${getErrorMessage(error)}`,
        { cause: error },
      );
    }
  }

  static async decryptDatabaseToBuffer(encryptedPath: string): Promise<Buffer> {
    if (!fs.existsSync(encryptedPath)) {
      throw new Error(
        `Encrypted database file does not exist: ${encryptedPath}`,
      );
    }

    let metadata: EncryptedFileMetadata;
    let encryptedData: Buffer;

    const fileBuffer = fs.readFileSync(encryptedPath);

    try {
      const metadataLength = fileBuffer.readUInt32BE(0);
      const metadataEnd = 4 + metadataLength;

      if (
        metadataLength <= 0 ||
        metadataEnd > fileBuffer.length ||
        metadataEnd <= 4
      ) {
        throw new Error("Invalid metadata length in single-file format");
      }

      const metadataJson = fileBuffer.slice(4, metadataEnd).toString("utf8");
      metadata = JSON.parse(metadataJson);
      encryptedData = fileBuffer.slice(metadataEnd);

      if (!metadata.iv || !metadata.tag || !metadata.version) {
        throw new Error("Invalid metadata structure in single-file format");
      }
    } catch (singleFileError) {
      const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
      if (!fs.existsSync(metadataPath)) {
        throw new Error(
          `Could not read database: Not a valid single-file format and metadata file is missing: ${metadataPath}. Error: ${singleFileError.message}`,
          { cause: singleFileError },
        );
      }

      try {
        const metadataContent = fs.readFileSync(metadataPath, "utf8");
        metadata = JSON.parse(metadataContent);
        encryptedData = fileBuffer;
      } catch (twoFileError) {
        throw new Error(
          `Failed to read database using both single-file and two-file formats. Error: ${twoFileError.message}`,
          { cause: twoFileError },
        );
      }
    }

    try {
      if (
        metadata.dataSize !== undefined &&
        encryptedData.length !== metadata.dataSize
      ) {
        databaseLogger.error(
          "Encrypted file size mismatch - possible corrupted write or mismatched metadata",
          null,
          {
            operation: "database_file_size_mismatch",
            encryptedPath,
            actualSize: encryptedData.length,
            expectedSize: metadata.dataSize,
          },
        );
        throw new Error(
          `Encrypted file size mismatch: expected ${metadata.dataSize} bytes but got ${encryptedData.length} bytes. ` +
            `This indicates corrupted files or interrupted write operation.`,
        );
      }

      let key: Buffer;
      if (metadata.version === "v2") {
        key = await this.systemCrypto.getDatabaseKey();
      } else if (metadata.version === "v1") {
        databaseLogger.warn(
          "Decrypting legacy v1 encrypted database - consider upgrading",
          {
            operation: "decrypt_legacy_v1",
            path: encryptedPath,
          },
        );
        if (!metadata.salt) {
          throw new Error("v1 encrypted file missing required salt field");
        }
        const salt = Buffer.from(metadata.salt, "hex");
        const fixedSeed =
          process.env.DB_FILE_KEY || "termix-database-file-encryption-seed-v1";
        key = crypto.pbkdf2Sync(fixedSeed, salt, 100000, 32, "sha256");
      } else {
        throw new Error(`Unsupported encryption version: ${metadata.version}`);
      }

      const decipher = crypto.createDecipheriv(
        metadata.algorithm,
        key,
        Buffer.from(metadata.iv, "hex"),
      ) as crypto.DecipherGCM;
      decipher.setAuthTag(Buffer.from(metadata.tag, "hex"));

      const decryptedBuffer = Buffer.concat([
        decipher.update(encryptedData),
        decipher.final(),
      ]);

      return decryptedBuffer;
    } catch (error) {
      const errorMessage = getErrorMessage(error);
      const isAuthError =
        errorMessage.includes("Unsupported state") ||
        errorMessage.includes("authenticate data") ||
        errorMessage.includes("auth");

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

        let envFileExists = false;
        let envFileReadable = false;
        try {
          envFileExists = fs.existsSync(envPath);
          if (envFileExists) {
            fs.accessSync(envPath, fs.constants.R_OK);
            envFileReadable = true;
          }
        } catch {
          // expected - env file access check may fail
        }

        databaseLogger.error(
          "Database decryption authentication failed - possible causes: wrong DATABASE_KEY, corrupted files, or interrupted write",
          error,
          {
            operation: "database_buffer_decryption_auth_failed",
            encryptedPath,
            dataDir,
            envPath,
            envFileExists,
            envFileReadable,
            hasEnvKey: !!process.env.DATABASE_KEY,
            envKeyLength: process.env.DATABASE_KEY?.length || 0,
            suggestion:
              "Check if DATABASE_KEY in .env matches the key used for encryption",
          },
        );
        throw new Error(
          `Database decryption authentication failed. This usually means:\n` +
            `1. DATABASE_KEY has changed or is missing from ${dataDir}/.env\n` +
            `2. Encrypted file was corrupted during write (system crash/restart)\n` +
            `3. Metadata file does not match encrypted data\n` +
            `\nDebug info:\n` +
            `- DATA_DIR: ${dataDir}\n` +
            `- .env file exists: ${envFileExists}\n` +
            `- .env file readable: ${envFileReadable}\n` +
            `- DATABASE_KEY in environment: ${!!process.env.DATABASE_KEY}\n` +
            `Original error: ${errorMessage}`,
          { cause: error },
        );
      }

      databaseLogger.error("Failed to decrypt database to buffer", error, {
        operation: "database_buffer_decryption_failed",
        encryptedPath,
        errorMessage,
      });
      throw new Error(`Database buffer decryption failed: ${errorMessage}`, {
        cause: error,
      });
    }
  }

  static async decryptDatabaseFile(
    encryptedPath: string,
    targetPath?: string,
  ): Promise<string> {
    const decryptedPath =
      targetPath || encryptedPath.replace(this.ENCRYPTED_FILE_SUFFIX, "");

    try {
      const decryptedBuffer = await this.decryptDatabaseToBuffer(encryptedPath);

      fs.writeFileSync(decryptedPath, decryptedBuffer);

      databaseLogger.info("Database file decrypted successfully", {
        operation: "database_file_decryption",
        encryptedPath,
        decryptedPath,
        decryptedSize: decryptedBuffer.length,
      });

      return decryptedPath;
    } catch (error) {
      databaseLogger.error("Failed to decrypt database file", error, {
        operation: "database_file_decryption_failed",
        encryptedPath,
        targetPath: decryptedPath,
      });
      throw new Error(
        `Database file decryption failed: ${getErrorMessage(error)}`,
        { cause: error },
      );
    }
  }

  static isEncryptedDatabaseFile(filePath: string): boolean {
    if (!fs.existsSync(filePath)) {
      return false;
    }

    const metadataPath = `${filePath}${this.METADATA_FILE_SUFFIX}`;
    if (fs.existsSync(metadataPath)) {
      try {
        const metadataContent = fs.readFileSync(metadataPath, "utf8");
        const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
        if (
          metadata.version === this.VERSION &&
          metadata.algorithm === this.ALGORITHM
        ) {
          return true;
        }
      } catch {
        // .meta parse failed, fall through to single-file detection
      }
    }

    try {
      const fileBuffer = fs.readFileSync(filePath);
      if (fileBuffer.length < 4) return false;

      const metadataLength = fileBuffer.readUInt32BE(0);
      const metadataEnd = 4 + metadataLength;

      if (metadataLength <= 0 || metadataEnd > fileBuffer.length) {
        return false;
      }

      const metadataJson = fileBuffer.slice(4, metadataEnd).toString("utf8");
      const metadata: EncryptedFileMetadata = JSON.parse(metadataJson);

      return (
        metadata.version === this.VERSION &&
        metadata.algorithm === this.ALGORITHM &&
        !!metadata.iv &&
        !!metadata.tag
      );
    } catch {
      return false;
    }
  }

  static getEncryptedFileInfo(encryptedPath: string): {
    version: string;
    algorithm: string;
    fingerprint: string;
    isCurrentHardware: boolean;
    fileSize: number;
  } | null {
    if (!this.isEncryptedDatabaseFile(encryptedPath)) {
      return null;
    }

    try {
      const fileStats = fs.statSync(encryptedPath);
      let metadata: EncryptedFileMetadata | null = null;

      const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
      if (fs.existsSync(metadataPath)) {
        try {
          const metadataContent = fs.readFileSync(metadataPath, "utf8");
          metadata = JSON.parse(metadataContent);
        } catch {
          // .meta parse failed, try single-file format
        }
      }

      if (!metadata) {
        const fileBuffer = fs.readFileSync(encryptedPath);
        const metadataLength = fileBuffer.readUInt32BE(0);
        const metadataEnd = 4 + metadataLength;
        const metadataJson = fileBuffer
          .subarray(4, metadataEnd)
          .toString("utf8");
        metadata = JSON.parse(metadataJson);
      }

      if (!metadata) {
        return null;
      }

      return {
        version: metadata.version,
        algorithm: metadata.algorithm,
        fingerprint: metadata.fingerprint,
        isCurrentHardware: true,
        fileSize: fileStats.size,
      };
    } catch {
      return null;
    }
  }

  static getDiagnosticInfo(encryptedPath: string): {
    dataFile: {
      exists: boolean;
      size?: number;
      mtime?: string;
      readable?: boolean;
    };
    metadataFile: {
      exists: boolean;
      size?: number;
      mtime?: string;
      readable?: boolean;
      content?: EncryptedFileMetadata;
    };
    environment: {
      dataDir: string;
      envPath: string;
      envFileExists: boolean;
      envFileReadable: boolean;
      hasEnvKey: boolean;
      envKeyLength: number;
    };
    validation: {
      filesConsistent: boolean;
      sizeMismatch?: boolean;
      expectedSize?: number;
      actualSize?: number;
    };
  } {
    const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
    const dataDir = process.env.DATA_DIR || "./db/data";
    const envPath = path.join(dataDir, ".env");

    const result: ReturnType<typeof this.getDiagnosticInfo> = {
      dataFile: { exists: false },
      metadataFile: { exists: false },
      environment: {
        dataDir,
        envPath,
        envFileExists: false,
        envFileReadable: false,
        hasEnvKey: !!process.env.DATABASE_KEY,
        envKeyLength: process.env.DATABASE_KEY?.length || 0,
      },
      validation: {
        filesConsistent: false,
      },
    };

    try {
      result.dataFile.exists = fs.existsSync(encryptedPath);
      if (result.dataFile.exists) {
        try {
          fs.accessSync(encryptedPath, fs.constants.R_OK);
          result.dataFile.readable = true;
          const stats = fs.statSync(encryptedPath);
          result.dataFile.size = stats.size;
          result.dataFile.mtime = stats.mtime.toISOString();
        } catch {
          result.dataFile.readable = false;
        }
      }

      result.metadataFile.exists = fs.existsSync(metadataPath);
      if (result.metadataFile.exists) {
        try {
          fs.accessSync(metadataPath, fs.constants.R_OK);
          result.metadataFile.readable = true;
          const stats = fs.statSync(metadataPath);
          result.metadataFile.size = stats.size;
          result.metadataFile.mtime = stats.mtime.toISOString();

          const content = fs.readFileSync(metadataPath, "utf8");
          result.metadataFile.content = JSON.parse(content);
        } catch {
          result.metadataFile.readable = false;
        }
      }

      result.environment.envFileExists = fs.existsSync(envPath);
      if (result.environment.envFileExists) {
        try {
          fs.accessSync(envPath, fs.constants.R_OK);
          result.environment.envFileReadable = true;
        } catch {
          // expected - env file access check may fail
        }
      }

      if (
        result.dataFile.exists &&
        result.metadataFile.exists &&
        result.metadataFile.content
      ) {
        result.validation.filesConsistent = true;

        if (result.metadataFile.content.dataSize !== undefined) {
          result.validation.expectedSize = result.metadataFile.content.dataSize;
          result.validation.actualSize = result.dataFile.size;
          result.validation.sizeMismatch =
            result.metadataFile.content.dataSize !== result.dataFile.size;
          if (result.validation.sizeMismatch) {
            result.validation.filesConsistent = false;
          }
        }
      }
    } catch (error) {
      databaseLogger.error("Failed to generate diagnostic info", error, {
        operation: "diagnostic_info_failed",
        encryptedPath,
      });
    }

    databaseLogger.info("Database encryption diagnostic info", {
      operation: "diagnostic_info_generated",
      ...result,
    });

    return result;
  }

  static async createEncryptedBackup(
    databasePath: string,
    backupDir: string,
  ): Promise<string> {
    if (!fs.existsSync(databasePath)) {
      throw new Error(`Database file does not exist: ${databasePath}`);
    }

    if (!fs.existsSync(backupDir)) {
      fs.mkdirSync(backupDir, { recursive: true });
    }

    const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
    const backupFileName = `database-backup-${timestamp}.sqlite.encrypted`;
    const backupPath = path.join(backupDir, backupFileName);

    try {
      const encryptedPath = await this.encryptDatabaseFile(
        databasePath,
        backupPath,
      );

      return encryptedPath;
    } catch (error) {
      databaseLogger.error("Failed to create encrypted backup", error, {
        operation: "database_backup_failed",
        sourcePath: databasePath,
        backupDir,
      });
      throw error;
    }
  }

  static async restoreFromEncryptedBackup(
    backupPath: string,
    targetPath: string,
  ): Promise<string> {
    if (!this.isEncryptedDatabaseFile(backupPath)) {
      throw new Error("Invalid encrypted backup file");
    }

    try {
      const restoredPath = await this.decryptDatabaseFile(
        backupPath,
        targetPath,
      );

      return restoredPath;
    } catch (error) {
      databaseLogger.error("Failed to restore from encrypted backup", error, {
        operation: "database_restore_failed",
        backupPath,
        targetPath,
      });
      throw error;
    }
  }

  static cleanupTempFiles(basePath: string): void {
    try {
      const tempFiles = [
        `${basePath}.tmp`,
        `${basePath}${this.ENCRYPTED_FILE_SUFFIX}`,
        `${basePath}${this.ENCRYPTED_FILE_SUFFIX}${this.METADATA_FILE_SUFFIX}`,
      ];

      for (const tempFile of tempFiles) {
        if (fs.existsSync(tempFile)) {
          fs.unlinkSync(tempFile);
        }
      }
    } catch (error) {
      databaseLogger.warn("Failed to clean up temporary files", {
        operation: "temp_cleanup_failed",
        basePath,
        error: getErrorMessage(error),
      });
    }
  }
}

export { DatabaseFileEncryption };
export type { EncryptedFileMetadata };
