/**
 * On-disk keystore — PLAN_RELAY_AGENT T3 (INV4/INV5). Persists ONLY to `stateDir`, every secret
 * file mode `0600`. Stores: the Ed25519 private key (PKCS#8 PEM), the mTLS cert + CA chain, and
 * the FIX 3 unwrapped `hostContentSecret`. The raw pairing code is NEVER written here (T4).
 */
import {
  chmodSync,
  existsSync,
  mkdirSync,
  readFileSync,
  statSync,
  writeFileSync,
} from 'node:fs'
import { join } from 'node:path'
import { createPrivateKey } from 'node:crypto'
import type { AgentIdentity } from './identity.js'
import { identityFromPrivatePem, p256IdentityFromPrivatePem } from './identity.js'

const SECRET_MODE = 0o600
const DIR_MODE = 0o700

export interface Keystore {
  saveIdentity(id: AgentIdentity): void
  loadIdentity(): AgentIdentity | null
  saveCert(certPem: string, caChainPem: string): void
  loadCert(): { certPem: string; caChainPem: string } | null
  saveContentSecret(secret: Uint8Array): void
  loadContentSecret(): Uint8Array | null
}

const KEY_FILE = 'agent.key.pem'
const CERT_FILE = 'agent.cert.pem'
const CA_FILE = 'agent.ca.pem'
const CONTENT_SECRET_FILE = 'content.secret'

/** Corrupt/unreadable key material surfaces as a typed error (never a silent empty read). */
export class KeystoreError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'KeystoreError'
  }
}

function ensureDir(stateDir: string): void {
  if (!existsSync(stateDir)) {
    mkdirSync(stateDir, { recursive: true, mode: DIR_MODE })
    return
  }
  // Refuse to write secrets into a world-/group-accessible directory (INV5 hard error).
  const mode = statSync(stateDir).mode & 0o777
  if ((mode & 0o077) !== 0) {
    throw new KeystoreError(
      `stateDir ${stateDir} is group/world accessible (mode ${mode.toString(8)}); refusing to write secrets`,
    )
  }
}

/**
 * Reconstruct an `AgentIdentity` from a stored PKCS#8 PEM, branching on the key's algorithm
 * discriminant (the PKCS#8 AlgorithmIdentifier OID, surfaced as `asymmetricKeyType`): an Ed25519
 * key → the relay-path builder; an EC P-256 key → the native frp-client builder (EC SPKI publicKey).
 * A saved P-256 identity therefore round-trips as `alg: 'p256'` with a usable signing key, while
 * Ed25519 stays byte-identical. Any other algorithm is a hard error (never silently mislabeled).
 */
function identityFromStoredPem(pem: string): AgentIdentity {
  const key = createPrivateKey(pem)
  const alg = key.asymmetricKeyType
  if (alg === 'ed25519') return identityFromPrivatePem(pem)
  if (alg === 'ec') {
    // An `ec` key alone is not proof of P-256 — a P-384/secp256k1 key also reports `ec`. The native
    // frp-client path is P-256 ONLY, so assert the named curve before treating it as such; any other
    // curve is a hard error (never silently mislabeled as a usable P-256 identity).
    const curve = key.asymmetricKeyDetails?.namedCurve
    if (curve !== 'prime256v1') {
      throw new Error(
        `unsupported stored EC identity curve: ${curve ?? 'unknown'} (only prime256v1/P-256 is supported)`,
      )
    }
    return p256IdentityFromPrivatePem(pem)
  }
  throw new Error(`unsupported stored identity key algorithm: ${alg ?? 'unknown'}`)
}

function writeSecret(path: string, data: string | Uint8Array): void {
  writeFileSync(path, data, { mode: SECRET_MODE })
  // Enforce 0600 even if a prior umask/file left it wider.
  chmodSync(path, SECRET_MODE)
}

export function openKeystore(stateDir: string): Keystore {
  const keyPath = join(stateDir, KEY_FILE)
  const certPath = join(stateDir, CERT_FILE)
  const caPath = join(stateDir, CA_FILE)
  const secretPath = join(stateDir, CONTENT_SECRET_FILE)

  return {
    saveIdentity(id: AgentIdentity): void {
      ensureDir(stateDir)
      writeSecret(keyPath, id.exportPrivatePkcs8Pem())
    },
    loadIdentity(): AgentIdentity | null {
      if (!existsSync(keyPath)) return null
      let pem: string
      try {
        pem = readFileSync(keyPath, 'utf8')
      } catch (err) {
        throw new KeystoreError(`failed to read identity key: ${(err as Error).message}`)
      }
      try {
        return identityFromStoredPem(pem)
      } catch (err) {
        throw new KeystoreError(`corrupt identity key file: ${(err as Error).message}`)
      }
    },
    saveCert(certPem: string, caChainPem: string): void {
      ensureDir(stateDir)
      writeSecret(certPath, certPem)
      writeSecret(caPath, caChainPem)
    },
    loadCert(): { certPem: string; caChainPem: string } | null {
      if (!existsSync(certPath) || !existsSync(caPath)) return null
      return {
        certPem: readFileSync(certPath, 'utf8'),
        caChainPem: readFileSync(caPath, 'utf8'),
      }
    },
    saveContentSecret(secret: Uint8Array): void {
      ensureDir(stateDir)
      writeSecret(secretPath, secret)
    },
    loadContentSecret(): Uint8Array | null {
      if (!existsSync(secretPath)) return null
      return new Uint8Array(readFileSync(secretPath))
    },
  }
}
