import fs from "fs"
import path from "path"
import crypto from "crypto"
import { Global } from "@opencode-ai/core/global"

interface WrappedDekData {
  version: number
  algorithm: "aes-256-gcm"
  kekId: string
  encryptedDek: string
  iv: string
  authTag: string
  timeStamp: number
}

export interface EncryptedBlob {
  version: number
  algorithm: "aes-256-gcm"
  ciphertext: string
  iv: string
  authTag: string
  timeStamp: number
}

const SENSITIVE_AUTH_KEYS = new Set(["access", "refresh", "key", "token"])

const algorithm: "aes-256-gcm" = "aes-256-gcm"
const ivLength = 12
const kekLength = 32
const dekLength = 32
const rootKeyIds = ["kek-v1", "kek-v2", "kek-v3"] as const

const configPath = Global.Path.config
const keyDirPath = path.join(configPath, "keys")
const wrappedDekPath = path.join(configPath, "token.dek")

function getRootKeyPath(keyId: string): string {
  return path.join(keyDirPath, `${keyId}.bin`)
}

function ensureDirectories() {
  if (!fs.existsSync(configPath)) fs.mkdirSync(configPath, { recursive: true, mode: 0o700 })
  if (!fs.existsSync(keyDirPath)) fs.mkdirSync(keyDirPath, { recursive: true, mode: 0o700 })
}

function ensureRootKeys() {
  ensureDirectories()
  for (const keyId of rootKeyIds) {
    const filePath = getRootKeyPath(keyId)
    if (fs.existsSync(filePath)) continue
    fs.writeFileSync(filePath, crypto.randomBytes(kekLength), { mode: 0o600 })
  }
}

function loadRootKey(keyId: string): Buffer {
  ensureRootKeys()
  const filePath = getRootKeyPath(keyId)
  const key = fs.readFileSync(filePath)
  if (key.length === kekLength) return key
  const next = crypto.randomBytes(kekLength)
  fs.writeFileSync(filePath, next, { mode: 0o600 })
  return next
}

function wrapDekWithKek(dek: Buffer, kekId: string): WrappedDekData {
  const iv = crypto.randomBytes(ivLength)
  const kek = loadRootKey(kekId)
  const cipher = crypto.createCipheriv(algorithm, kek, iv) as crypto.CipherGCM
  const encryptedDek = Buffer.concat([cipher.update(dek), cipher.final()])
  const authTag = cipher.getAuthTag()
  return {
    version: 1,
    algorithm,
    kekId,
    encryptedDek: encryptedDek.toString("base64"),
    iv: iv.toString("base64"),
    authTag: authTag.toString("base64"),
    timeStamp: Date.now(),
  }
}

function unwrapDek(wrapped: WrappedDekData): Buffer {
  const kek = loadRootKey(wrapped.kekId)
  const iv = Buffer.from(wrapped.iv, "base64")
  const authTag = Buffer.from(wrapped.authTag, "base64")
  const encryptedDek = Buffer.from(wrapped.encryptedDek, "base64")
  const decipher = crypto.createDecipheriv(algorithm, kek, iv) as crypto.DecipherGCM
  decipher.setAuthTag(authTag)
  return Buffer.concat([decipher.update(encryptedDek), decipher.final()])
}

function ensureWrappedDek() {
  ensureRootKeys()
  if (fs.existsSync(wrappedDekPath)) return
  const dek = crypto.randomBytes(dekLength)
  const wrapped = wrapDekWithKek(dek, rootKeyIds[0])
  fs.writeFileSync(wrappedDekPath, JSON.stringify(wrapped, null, 2), { mode: 0o600 })
}

function loadDek(): Buffer {
  ensureWrappedDek()
  const wrapped = JSON.parse(fs.readFileSync(wrappedDekPath, "utf8")) as WrappedDekData
  const dek = unwrapDek(wrapped)
  if (dek.length === dekLength) return dek
  const next = crypto.randomBytes(dekLength)
  const nextWrapped = wrapDekWithKek(next, rootKeyIds[0])
  fs.writeFileSync(wrappedDekPath, JSON.stringify(nextWrapped, null, 2), { mode: 0o600 })
  return next
}

export function encryptForLocalStorage(plaintext: string): EncryptedBlob {
  const dek = loadDek()
  const iv = crypto.randomBytes(ivLength)
  const cipher = crypto.createCipheriv(algorithm, dek, iv) as crypto.CipherGCM
  const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()])
  const authTag = cipher.getAuthTag()
  return {
    version: 1,
    algorithm,
    ciphertext: ciphertext.toString("base64"),
    iv: iv.toString("base64"),
    authTag: authTag.toString("base64"),
    timeStamp: Date.now(),
  }
}

export function decryptForLocalStorage(blob: EncryptedBlob): string {
  try {
    const dek = loadDek()
    const iv = Buffer.from(blob.iv, "base64")
    const authTag = Buffer.from(blob.authTag, "base64")
    const ciphertext = Buffer.from(blob.ciphertext, "base64")
    const decipher = crypto.createDecipheriv(algorithm, dek, iv) as crypto.DecipherGCM
    decipher.setAuthTag(authTag)
    return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8")
  } catch (cause) {
    throw new Error("Failed to decrypt local ciphertext", { cause })
  }
}

export function isEncryptedBlob(value: unknown): value is EncryptedBlob {
  if (!value || typeof value !== "object") return false
  const candidate = value as Partial<EncryptedBlob>
  return (
    candidate.algorithm === "aes-256-gcm" &&
    typeof candidate.ciphertext === "string" &&
    typeof candidate.iv === "string" &&
    typeof candidate.authTag === "string"
  )
}

function encryptAuthRecord(info: Record<string, unknown>): Record<string, unknown> {
  return Object.fromEntries(
    Object.entries(info).map(([field, value]) => {
      if (typeof value !== "string") return [field, value]
      if (!SENSITIVE_AUTH_KEYS.has(field)) return [field, value]
      return [field, encryptForLocalStorage(value)]
    }),
  )
}

function decryptAuthRecord(provider: string, info: Record<string, unknown>): Record<string, unknown> {
  return Object.fromEntries(
    Object.entries(info).map(([field, value]) => {
      if (!SENSITIVE_AUTH_KEYS.has(field)) return [field, value]
      if (!isEncryptedBlob(value)) return [field, value]
      try {
        return [field, decryptForLocalStorage(value)]
      } catch (cause) {
        throw new Error(`Failed to decrypt auth field "${field}" for provider "${provider}"`, {
          cause,
        })
      }
    }),
  )
}

export function encryptAuthData(data: Record<string, unknown>): Record<string, unknown> {
  return Object.fromEntries(
    Object.entries(data).map(([provider, info]) => {
      if (!info || typeof info !== "object") return [provider, info]
      return [provider, encryptAuthRecord(info as Record<string, unknown>)]
    }),
  )
}

export function decryptAuthData(data: Record<string, unknown>): Record<string, unknown> {
  return Object.fromEntries(
    Object.entries(data).map(([provider, info]) => {
      if (!info || typeof info !== "object") return [provider, info]
      return [provider, decryptAuthRecord(provider, info as Record<string, unknown>)]
    }),
  )
}

export * as LocalCrypto from "./local-crypto"