package dev.ulfrx.recipe.auth

import dev.ulfrx.recipe.shared.dto.MeResponse
import io.ktor.server.auth.jwt.JWTPrincipal
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.sql.TextColumnType
import org.jetbrains.exposed.sql.statements.StatementType
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction

/**
 * Resolves a Ktor [JWTPrincipal] to a persisted [MeResponse] via JIT upsert (D-25).
 *
 * One row per OIDC `sub`; email + display_name refresh on every authenticated
 * request so claim drift in Authentik propagates without an explicit migration.
 *
 * Implementation detail: Postgres `INSERT ... ON CONFLICT (sub) DO UPDATE ...
 * RETURNING *` is the atomic upsert per D-25. We issue raw SQL via Exposed's
 * `exec()` so we can use `RETURNING` (Exposed 0.55.0 DSL `upsert` returns the
 * insert count, not the row, and we need the generated id and updated_at). DAO
 * is forbidden by CLAUDE.md #5; we use `exec` (DSL) not the DAO API.
 *
 * `newSuspendedTransaction(Dispatchers.IO)` is mandatory: a plain blocking
 * Exposed transaction inside a suspend route exhausts the connection pool
 * (CLAUDE.md #6, PITFALLS.md #5/#6).
 */
public class PrincipalResolver {
    public suspend fun resolve(principal: JWTPrincipal): MeResponse {
        val sub =
            principal.payload.subject?.takeIf { it.isNotBlank() }
                ?: error("PrincipalResolver invoked with blank sub — should be blocked by AuthPlugin.validate")
        val email =
            principal.payload
                .getClaim("email")
                ?.asString()
                .orEmpty()
        val nameClaim = principal.payload.getClaim("name")?.asString()
        val preferredUsername = principal.payload.getClaim("preferred_username")?.asString()
        val displayName = nameClaim ?: preferredUsername ?: email.ifBlank { sub }

        return newSuspendedTransaction(Dispatchers.IO) {
            var row: MeResponse? = null
            // INSERT...RETURNING returns a ResultSet, so we must mark this as a SELECT
            // statement type — Exposed's `exec` defaults to executeUpdate() for INSERT
            // statements which Postgres rejects with "A result was returned when none
            // was expected."
            exec(
                stmt =
                    """
                    INSERT INTO users (sub, email, display_name)
                    VALUES (?, ?, ?)
                    ON CONFLICT (sub) DO UPDATE
                        SET email = EXCLUDED.email,
                            display_name = EXCLUDED.display_name,
                            updated_at = now()
                    RETURNING id, sub, email, display_name
                    """.trimIndent(),
                args =
                    listOf(
                        TextColumnType() to sub,
                        TextColumnType() to email,
                        TextColumnType() to displayName,
                    ),
                explicitStatementType = StatementType.SELECT,
            ) { rs ->
                if (rs.next()) {
                    row =
                        MeResponse(
                            id = rs.getObject("id").toString(),
                            sub = rs.getString("sub"),
                            email = rs.getString("email"),
                            displayName = rs.getString("display_name"),
                        )
                }
            }
            row ?: error("Upsert RETURNING produced no row for sub=$sub")
        }
    }
}
