/**
 * Request dispatch — context GET and mutation POST handlers.
 *
 * Framework-agnostic. Returns plain objects. The router adapter
 * (Express, Hono, etc.) converts to framework-specific responses.
 */

import { getFunction, getContextGroups } from './registry'
import { resolveInvalidation, formatInvalidateHeader } from './invalidation'
import { getCache, cacheGet, cachePut, cachePurge } from './cache'
import { ANONYMOUS, type Identity } from './identity'
import type { AuthRequirement } from './types'

let _cacheSecret: string | null = null

/** Set the cache secret for origin-side caching. */
export function setCacheSecret(secret: string | null): void {
    _cacheSecret = secret
}

export interface MizanResponse {
    status: number
    body: any
    headers: Record<string, string>
}

interface AuthDenial {
    status: 401 | 403
    code: 'UNAUTHORIZED' | 'FORBIDDEN'
    message: string
}

/**
 * Check whether `identity` satisfies the stored `auth` requirement.
 * Ports Django's _check_auth_requirement exactly. Returns an AuthDenial
 * on failure, or null when access is allowed.
 */
function checkAuth(auth: AuthRequirement | undefined, identity: Identity): AuthDenial | null {
    if (auth === undefined) return null

    // Callable runs first — before the authentication gate.
    if (typeof auth === 'function') {
        try {
            return auth(identity)
                ? null
                : { status: 403, code: 'FORBIDDEN', message: 'Access denied' }
        } catch (e: any) {
            return { status: 403, code: 'FORBIDDEN', message: e?.message || 'Access denied' }
        }
    }

    if (!identity.isAuthenticated) {
        return { status: 401, code: 'UNAUTHORIZED', message: 'Authentication required' }
    }

    if (auth === 'staff' && !identity.isStaff) {
        return { status: 403, code: 'FORBIDDEN', message: 'Staff access required' }
    }

    if (auth === 'superuser' && !identity.isSuperuser) {
        return { status: 403, code: 'FORBIDDEN', message: 'Superuser access required' }
    }

    return null
}

function authDenialResponse(denial: AuthDenial): MizanResponse {
    return {
        status: denial.status,
        body: { error: true, code: denial.code, message: denial.message },
        headers: { 'Cache-Control': 'no-store', 'Content-Type': 'application/json' },
    }
}

/**
 * Handle GET /api/mizan/ctx/:contextName/
 *
 * Bundles all functions in a named context into one response.
 */
export async function handleContextFetch(
    contextName: string,
    params: Record<string, string>,
    identity: Identity = ANONYMOUS,
): Promise<MizanResponse> {
    const groups = getContextGroups()
    const fnNames = groups[contextName]

    if (!fnNames) {
        return {
            status: 404,
            body: { error: true, code: 'NOT_FOUND', message: `Context '${contextName}' not found` },
            headers: { 'Cache-Control': 'no-store', 'Content-Type': 'application/json' },
        }
    }

    // Auth pre-pass — run BEFORE the cache lookup so a cache HIT can never
    // leak to an unauthorized caller. Any denial short-circuits, uncached.
    for (const fnName of fnNames) {
        const entry = getFunction(fnName)
        if (!entry) continue
        const denial = checkAuth(entry.auth, identity)
        if (denial) return authDenialResponse(denial)
    }

    // Resolve effective rev (max across functions) and cache policy (min TTL)
    let effectiveRev = 0
    for (const fnName of fnNames) {
        const entry = getFunction(fnName)
        if (entry?.rev) effectiveRev = Math.max(effectiveRev, entry.rev)
    }

    // Origin-side cache lookup
    const cacheBackend = getCache()
    const cacheSecret = _cacheSecret
    if (cacheBackend && cacheSecret) {
        try {
            const cached = cacheGet(cacheSecret, cacheBackend, contextName, params, undefined, effectiveRev)
            if (cached !== null) {
                return {
                    status: 200,
                    body: JSON.parse(cached),
                    headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'X-Mizan-Cache': 'HIT' },
                }
            }
        } catch { /* cache miss on error */ }
    }

    const results: Record<string, any> = {}

    for (const fnName of fnNames) {
        const entry = getFunction(fnName)
        if (!entry) continue

        // Filter params to only those this function declares
        const fnParams: Record<string, any> = {}
        for (const p of entry.params) {
            if (p.name in params) fnParams[p.name] = params[p.name]
        }

        try {
            const argValues = entry.params.map(p => fnParams[p.name])
            const result = await entry.fn(...argValues)

            // View path — skip (context GET is for RPC data)
            if (result instanceof Response) continue

            results[fnName] = result
        } catch (e: any) {
            return {
                status: 500,
                body: { error: true, code: 'INTERNAL_ERROR', message: 'Internal error' },
                headers: { 'Cache-Control': 'no-store', 'Content-Type': 'application/json' },
            }
        }
    }

    // Resolve effective cache policy for origin-side cache decision
    let effectiveCache: number | boolean = true
    for (const fnName of fnNames) {
        const entry = getFunction(fnName)
        if (!entry) continue
        if (entry.cache === false) { effectiveCache = false; break }
        if (typeof entry.cache === 'number') {
            effectiveCache = effectiveCache === true
                ? entry.cache
                : Math.min(effectiveCache as number, entry.cache)
        }
    }

    // Store in origin-side cache (skip if cache=False)
    if (cacheBackend && cacheSecret && effectiveCache !== false) {
        try {
            cachePut(cacheSecret, cacheBackend, contextName, params, JSON.stringify(results), undefined, effectiveRev)
        } catch { /* cache store failure is non-fatal */ }
    }

    return {
        status: 200,
        body: results,
        headers: {
            'Content-Type': 'application/json',
            'Cache-Control': 'no-store',
            ...(cacheBackend && cacheSecret ? { 'X-Mizan-Cache': 'MISS' } : {}),
        },
    }
}

/**
 * Handle POST /api/mizan/call/
 *
 * Dispatches to a named function. Returns result + invalidation.
 */
export async function handleMutationCall(
    fnName: string,
    args: Record<string, any>,
    identity: Identity = ANONYMOUS,
): Promise<MizanResponse> {
    const entry = getFunction(fnName)

    if (!entry) {
        return {
            status: 404,
            body: { error: true, code: 'NOT_FOUND', message: `Function '${fnName}' not found` },
            headers: { 'Cache-Control': 'no-store', 'Content-Type': 'application/json' },
        }
    }

    // Reject private functions from RPC dispatch
    if (entry.private) {
        return {
            status: 403,
            body: { error: true, code: 'FORBIDDEN', message: 'Function is not client-callable' },
            headers: { 'Cache-Control': 'no-store', 'Content-Type': 'application/json' },
        }
    }

    // Auth enforcement — after private rejection, before execution.
    const denial = checkAuth(entry.auth, identity)
    if (denial) return authDenialResponse(denial)

    try {
        const argValues = entry.params.map(p => args[p.name])
        const result = await entry.fn(...argValues)

        // View path — return Response directly with invalidation header
        if (result instanceof Response) {
            const invalidate = resolveInvalidation(entry, args)
            if (invalidate) {
                result.headers.set('X-Mizan-Invalidate', formatInvalidateHeader(invalidate))
            }
            result.headers.set('Cache-Control', 'no-store')
            return {
                status: result.status,
                body: result,
                headers: Object.fromEntries(result.headers.entries()),
            }
        }

        // RPC path — JSON response with invalidation
        const invalidate = resolveInvalidation(entry, args)
        const responseData: Record<string, any> = { result }
        const headers: Record<string, string> = {
            'Content-Type': 'application/json',
            'Cache-Control': 'no-store',
        }

        if (invalidate) {
            responseData.invalidate = invalidate
            headers['X-Mizan-Invalidate'] = formatInvalidateHeader(invalidate)

            // Purge origin-side cache
            const cb = getCache()
            if (cb) {
                try {
                    for (const entry of invalidate) {
                        if (typeof entry === 'string') {
                            cachePurge(cb, entry)
                        } else {
                            cachePurge(cb, entry.context, entry.params, _cacheSecret)
                        }
                    }
                } catch { /* purge failure is non-fatal */ }
            }
        }

        return { status: 200, body: responseData, headers }
    } catch (e: any) {
        return {
            status: 500,
            body: { error: true, code: 'INTERNAL_ERROR', message: 'Internal error' },
            headers: { 'Cache-Control': 'no-store', 'Content-Type': 'application/json' },
        }
    }
}
