package no.zyon.shade.approval

import no.zyon.shade.crypto.CryptoProvider
import java.nio.ByteBuffer
import java.nio.ByteOrder

/**
 * V4.10 — cross-host approval routing helpers. Mirror
 * `@shade/sdk/approval.ts` byte-for-byte.
 *
 * The frames themselves (`approvalNeeded` / `linkApproveByProxy`) are
 * app-defined payloads sent over the existing Shade bilateral E2EE
 * channel. This file ships the canonical signing-payload layout, the
 * Ed25519 sign step a phone runs after biometric unlock, and the
 * verify step a host runs against the freshest profile blob.
 *
 * The signing payload is length-prefixed binary (u16 BE) so any
 * platform — Kotlin, Swift, Go — can produce byte-identical input
 * without needing a JSON canonicalizer. Cross-platform parity is
 * gated by `test-vectors/blob-storage.json` (signing payload
 * fixtures) plus a Kotlin↔TS round-trip in `CrossPlatformVectorTest`.
 */

/** Default domain separator. Apps with their own canonical name (e.g. Prism) override. */
const val DEFAULT_APPROVAL_DOMAIN = "shade-link-approve-v1"

/** Default expiry: 5 minutes after the host issues the request. */
const val DEFAULT_APPROVAL_EXPIRES_IN_MS = 5L * 60 * 1000

/** Information about the device the host received a `linkRequest` from. */
data class ApprovalRequestingDevice(
    val fingerprint: String,
    val deviceName: String? = null,
    val userAgent: String? = null,
    val ipHint: String? = null,
    val receivedAt: Long,
)

data class ApprovalRequestFrame(
    val kind: String = "approvalNeeded",
    /** 128-bit hex (32 chars) random idempotency key. */
    val requestId: String,
    val hostAddress: String,
    val hostFingerprint: String,
    val requestingDevice: ApprovalRequestingDevice,
    val expiresAt: Long,
    val domain: String,
)

data class ProxyApprovalFrame(
    val kind: String = "linkApproveByProxy",
    val requestId: String,
    val decision: String,
    val approverFingerprint: String,
    /** 64-byte Ed25519 signature, lowercase hex (128 chars). */
    val signature: String,
    val domain: String,
)

/**
 * Build a fresh `approvalNeeded` frame with a 128-bit random
 * `requestId`. Hosts SHOULD persist the requestId in a pending-set
 * keyed by `expiresAt` so a returning `linkApproveByProxy` can be
 * matched up — that's app state, the SDK doesn't track it.
 */
fun buildApprovalRequest(
    crypto: CryptoProvider,
    hostAddress: String,
    hostFingerprint: String,
    requestingDeviceFingerprint: String,
    deviceName: String? = null,
    userAgent: String? = null,
    ipHint: String? = null,
    expiresInMs: Long = DEFAULT_APPROVAL_EXPIRES_IN_MS,
    domain: String = DEFAULT_APPROVAL_DOMAIN,
    now: Long = System.currentTimeMillis(),
): ApprovalRequestFrame {
    val requestId = crypto.randomBytes(16).joinToString("") { "%02x".format(it) }
    return ApprovalRequestFrame(
        requestId = requestId,
        hostAddress = hostAddress,
        hostFingerprint = hostFingerprint,
        requestingDevice = ApprovalRequestingDevice(
            fingerprint = requestingDeviceFingerprint,
            deviceName = deviceName,
            userAgent = userAgent,
            ipHint = ipHint,
            receivedAt = now,
        ),
        expiresAt = now + expiresInMs,
        domain = domain,
    )
}

/**
 * Sign a `linkApproveByProxy` frame with the approver's long-term
 * Ed25519 identity key. The seed is the 32-byte Ed25519 private key
 * (Tink's `Ed25519Sign(seed)` consumes it directly).
 */
fun signProxyApproval(
    crypto: CryptoProvider,
    request: ApprovalRequestFrame,
    decision: String,
    approverFingerprint: String,
    approverSigningKey: ByteArray,
): ProxyApprovalFrame {
    require(decision == "approve" || decision == "reject") {
        "decision must be 'approve' or 'reject'"
    }
    require(approverSigningKey.size == 32) {
        "approverSigningKey must be 32 bytes (Ed25519 seed)"
    }
    val payload = canonicalApprovalSigningBytes(
        domain = request.domain,
        requestId = request.requestId,
        hostFingerprint = request.hostFingerprint,
        requestingDeviceFingerprint = request.requestingDevice.fingerprint,
        decision = decision,
    )
    val sig = crypto.sign(approverSigningKey, payload)
    return ProxyApprovalFrame(
        requestId = request.requestId,
        decision = decision,
        approverFingerprint = approverFingerprint,
        signature = sig.joinToString("") { "%02x".format(it) },
        domain = request.domain,
    )
}

/** Tagged result of `verifyProxyApproval`. */
sealed class VerifyProxyApprovalResult {
    data class Ok(val approver: ProfileClientEntry) : VerifyProxyApprovalResult()
    data class Failed(val reason: Reason) : VerifyProxyApprovalResult()

    enum class Reason {
        REQUEST_ID_MISMATCH,
        DOMAIN_MISMATCH,
        UNKNOWN_APPROVER,
        NOT_TRUSTED,
        BAD_SIGNATURE,
        EXPIRED,
    }
}

/**
 * Verify a `linkApproveByProxy` against the originating
 * `approvalNeeded` and the host's freshest profile blob. Returns a
 * tagged result rather than throwing — callers usually want to log
 * the reason before deciding what to surface to the user.
 *
 * Order of checks:
 *
 *   1. requestId match (replay defense)
 *   2. domain match (cross-app confusion defense)
 *   3. approver resolves to a `clients[]` entry
 *   4. approver is in `trustedApproverFingerprints[]` AND has the
 *      `trustedApprover` flag (cross-checked via `isTrustedApprover`)
 *   5. expiresAt in the future
 *   6. Ed25519 signature verifies against `clients[].identityPublicKey`
 *
 * Hosts MUST refetch the profile blob fresh before calling this — see
 * the FR §5 "approver-revocation propagation" rationale.
 */
fun verifyProxyApproval(
    crypto: CryptoProvider,
    request: ApprovalRequestFrame,
    approval: ProxyApprovalFrame,
    profile: CanonicalProfileBlob,
    now: Long = System.currentTimeMillis(),
): VerifyProxyApprovalResult {
    if (approval.requestId != request.requestId) {
        return VerifyProxyApprovalResult.Failed(
            VerifyProxyApprovalResult.Reason.REQUEST_ID_MISMATCH,
        )
    }
    if (approval.domain != request.domain) {
        return VerifyProxyApprovalResult.Failed(
            VerifyProxyApprovalResult.Reason.DOMAIN_MISMATCH,
        )
    }

    val approver = findClientByFingerprint(profile, approval.approverFingerprint)
        ?: return VerifyProxyApprovalResult.Failed(
            VerifyProxyApprovalResult.Reason.UNKNOWN_APPROVER,
        )

    if (!isTrustedApprover(profile, approval.approverFingerprint)) {
        return VerifyProxyApprovalResult.Failed(
            VerifyProxyApprovalResult.Reason.NOT_TRUSTED,
        )
    }

    if (now > request.expiresAt) {
        return VerifyProxyApprovalResult.Failed(VerifyProxyApprovalResult.Reason.EXPIRED)
    }

    val pubkey = try {
        hexToBytes(approver.identityPublicKey)
    } catch (_: Exception) {
        return VerifyProxyApprovalResult.Failed(VerifyProxyApprovalResult.Reason.BAD_SIGNATURE)
    }
    val sig = try {
        hexToBytes(approval.signature)
    } catch (_: Exception) {
        return VerifyProxyApprovalResult.Failed(VerifyProxyApprovalResult.Reason.BAD_SIGNATURE)
    }
    if (pubkey.size != 32 || sig.size != 64) {
        return VerifyProxyApprovalResult.Failed(VerifyProxyApprovalResult.Reason.BAD_SIGNATURE)
    }

    val payload = canonicalApprovalSigningBytes(
        domain = approval.domain,
        requestId = approval.requestId,
        hostFingerprint = request.hostFingerprint,
        requestingDeviceFingerprint = request.requestingDevice.fingerprint,
        decision = approval.decision,
    )
    val ok = crypto.verify(pubkey, payload, sig)
    return if (ok) VerifyProxyApprovalResult.Ok(approver)
    else VerifyProxyApprovalResult.Failed(VerifyProxyApprovalResult.Reason.BAD_SIGNATURE)
}

/**
 * Build the canonical signing payload bytes for a proxy approval.
 *
 * Format (length-prefixed UTF-8, big-endian u16 lengths):
 *
 *   u16(len(domain))     || domain
 *   u16(len(requestId))  || requestId
 *   u16(len(hostFp))     || hostFingerprint
 *   u16(len(requestFp))  || requestingDeviceFingerprint
 *   u16(len(decision))   || decision
 *
 * This is the EXACT byte layout `@shade/sdk`'s
 * `canonicalApprovalSigningBytes` produces, ensuring an Android-signed
 * approval verifies on a TS host and vice versa.
 */
fun canonicalApprovalSigningBytes(
    domain: String,
    requestId: String,
    hostFingerprint: String,
    requestingDeviceFingerprint: String,
    decision: String,
): ByteArray {
    val fields = listOf(
        domain.toByteArray(Charsets.UTF_8),
        requestId.toByteArray(Charsets.UTF_8),
        hostFingerprint.toByteArray(Charsets.UTF_8),
        requestingDeviceFingerprint.toByteArray(Charsets.UTF_8),
        decision.toByteArray(Charsets.UTF_8),
    )
    for (f in fields) {
        require(f.size <= 0xFFFF) { "signing field too long: ${f.size} bytes (max 65535)" }
    }
    val total = fields.sumOf { 2 + it.size }
    val buf = ByteBuffer.allocate(total).order(ByteOrder.BIG_ENDIAN)
    for (f in fields) {
        buf.putShort(f.size.toShort())
        buf.put(f)
    }
    return buf.array()
}

private fun hexToBytes(hex: String): ByteArray {
    require(hex.length % 2 == 0) { "hex length must be even" }
    require(hex.all { it.isDigit() || it in 'a'..'f' }) { "hex must be lowercase 0-9a-f" }
    val out = ByteArray(hex.length / 2)
    for (i in out.indices) {
        out[i] = ((Character.digit(hex[i * 2], 16) shl 4) +
            Character.digit(hex[i * 2 + 1], 16)).toByte()
    }
    return out
}
