/**
 * 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'

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>
}

/**
 * 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>,
): 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' },
        }
    }

    // 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>,
): 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' },
        }
    }

    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' },
        }
    }
}
