/**
 * mizan/client
 *
 * HTTP client factories for Django backends.
 * Framework-agnostic - works with vanilla JS, React, Vue, etc.
 *
 * ## Quick Start
 *
 * ### Client-Side (CSR)
 * ```ts
 * import { createMizanCSRClient, Auth } from 'mizan/client'
 *
 * // Session-based (cookies + CSRF)
 * const client = createMizanCSRClient(Auth.SESSION)
 *
 * // JWT-based (Bearer token)
 * const client = createMizanCSRClient(Auth.JWT, { getAccessToken })
 *
 * const user = await client.json('GET', '/api/accounts/me/')
 * ```
 *
 * ### Server-Side (SSR)
 * ```ts
 * import { createMizanSSRClient } from 'mizan/client'
 *
 * const client = createMizanSSRClient({
 *     cookies: await cookies() // Next.js cookies()
 * })
 *
 * const user = await client.json('GET', '/api/accounts/me/')
 * ```
 *
 * ## React Hooks
 *
 * For React, import from `/react`:
 * ```tsx
 * import { useDjangoCSRClient, Auth } from 'mizan/client/react'
 *
 * const client = useDjangoCSRClient(Auth.SESSION)
 * ```
 */

// =============================================================================
// Types
// =============================================================================

/**
 * Authentication strategy for client-side requests.
 */
export enum Auth {
    /** Session cookies with CSRF token */
    SESSION = 'session',
    /** JWT Bearer token */
    JWT = 'jwt',
}

/**
 * Cookie getter interface (matches Next.js cookies() return type).
 */
export interface CookieGetter {
    get(name: string): { name: string; value: string } | undefined
    getAll(): { name: string; value: string }[]
}

/**
 * Cookie configuration for SSR requests.
 * Can be either a cookie getter (like Next.js cookies()) or pre-extracted values.
 */
export type SSRCookies = CookieGetter | {
    /** CSRF token value */
    csrf: string
    /** Full cookie header string */
    cookieHeader: string
}

/**
 * The core HTTP client interface for Django requests.
 */
export interface MizanHTTPClient {
    /**
     * Make an HTTP request, returning the raw Response.
     */
    request(method: string, path: string, data?: unknown, headers?: Record<string, string>): Promise<Response>

    /**
     * Make an HTTP request, parsing the response as JSON.
     * @throws {HttpError} When response is not ok
     */
    json<T>(method: string, path: string, data?: unknown, headers?: Record<string, string>): Promise<T>
}

/**
 * Configuration for CSR client.
 */
export interface CSRClientConfig {
    /** Base URL for the Django backend */
    baseUrl?: string | (() => string)
}

/**
 * Configuration for JWT-authenticated CSR client.
 */
export interface JWTClientConfig extends CSRClientConfig {
    /** Async function that returns the current access token */
    getAccessToken: () => Promise<string | null>
}

/**
 * Configuration for SSR client.
 */
export interface SSRClientConfig {
    /** Cookies for authentication forwarding */
    cookies: SSRCookies
    /** Internal backend URL override (defaults to http://${INTERNAL_BACKEND_HOSTNAME}:8000) */
    baseUrl?: string
}

// =============================================================================
// Errors
// =============================================================================

/**
 * Details about an HTTP error.
 */
export interface HttpErrorDetails {
    status: number
    statusText: string
    url: string
    bodyJson?: unknown
    bodySnippet?: string
    bodyIsHtml?: boolean
}

/**
 * Error thrown when an HTTP request fails.
 */
export class HttpError extends Error {
    constructor(
        message: string,
        public readonly details: HttpErrorDetails
    ) {
        super(message)
        this.name = 'HttpError'
    }
}

// =============================================================================
// Internal Utilities
// =============================================================================

import { getCSRFToken, getCsrfHeaderName, getCsrfCookieName } from '../utils'

interface RequestBuild {
    request: RequestInit
    hasBody: boolean
}

function buildRequest(method: string, data?: unknown, headers?: Record<string, string>): RequestBuild {
    const isBodyMethod = !['GET', 'HEAD'].includes(method.toUpperCase())
    const hasBody = isBodyMethod && data !== undefined

    const requestHeaders: Record<string, string> = {
        'Accept': 'application/json',
        ...headers,
    }

    let body: BodyInit | undefined
    if (hasBody) {
        if (data instanceof FormData) {
            body = data
        } else {
            body = JSON.stringify(data)
            requestHeaders['Content-Type'] = 'application/json'
        }
    }

    return {
        request: {
            method: method.toUpperCase(),
            headers: requestHeaders,
            body,
            credentials: 'include',
        },
        hasBody,
    }
}

async function buildHttpError(resp: Response, url: URL | string): Promise<HttpError> {
    const urlStr = typeof url === 'string' ? url : url.toString()
    const details: HttpErrorDetails = {
        status: resp.status,
        statusText: resp.statusText,
        url: urlStr,
    }

    try {
        const contentType = resp.headers.get('content-type') ?? ''
        if (contentType.includes('application/json')) {
            details.bodyJson = await resp.clone().json()
        } else {
            const text = await resp.clone().text()
            details.bodyIsHtml = contentType.includes('text/html')
            details.bodySnippet = text.slice(0, 500)
        }
    } catch {
        // Ignore body parsing errors
    }

    return new HttpError(`Request failed: ${resp.status} ${resp.statusText}`, details)
}

// =============================================================================
// CSR Client Factory
// =============================================================================

/**
 * Create a client-side HTTP client for Django.
 *
 * @param auth - Authentication strategy (Auth.SESSION or Auth.JWT)
 * @param config - Client configuration
 * @returns MizanHTTPClient
 *
 * @example
 * // Session-based
 * const client = createMizanCSRClient(Auth.SESSION)
 *
 * @example
 * // JWT-based
 * const client = createMizanCSRClient(Auth.JWT, {
 *     getAccessToken: async () => localStorage.getItem('token')
 * })
 */
export function createMizanCSRClient(auth: Auth.SESSION, config?: CSRClientConfig): MizanHTTPClient
export function createMizanCSRClient(auth: Auth.JWT, config: JWTClientConfig): MizanHTTPClient
export function createMizanCSRClient(
    auth: Auth,
    config?: CSRClientConfig | JWTClientConfig
): MizanHTTPClient {
    if (!config?.baseUrl) {
        throw new Error(
            'baseUrl is required. Pass it via config or use MizanProvider which provides it automatically.'
        )
    }

    const getBaseUrl = () => typeof config.baseUrl === 'function' ? config.baseUrl() : config.baseUrl!

    const getHeaders = async (): Promise<Record<string, string>> => {
        if (auth === Auth.JWT) {
            const jwtConfig = config as JWTClientConfig
            const token = await jwtConfig.getAccessToken()
            if (token) {
                return { Authorization: `Bearer ${token}` }
            }
            return {}
        }
        // Session auth uses CSRF
        return { [getCsrfHeaderName()]: getCSRFToken() ?? '' }
    }

    function resolveUrl(path: string): string {
        const base = getBaseUrl()
        // Absolute base URL — use URL constructor
        if (base.startsWith('http://') || base.startsWith('https://')) {
            return new URL(path, base).toString()
        }
        // Relative base — path is already usable by fetch in a browser
        return path
    }

    return {
        request: async (method, path, data?, headers?) => {
            const url = resolveUrl(path)
            const configHeaders = await getHeaders()
            const build = buildRequest(method, data, { ...configHeaders, ...headers })
            return fetch(url, build.request)
        },

        json: async <T>(method: string, path: string, data?: unknown, headers?: Record<string, string>): Promise<T> => {
            const url = resolveUrl(path)
            const configHeaders = await getHeaders()
            const build = buildRequest(method, data, { ...configHeaders, ...headers })
            const resp = await fetch(url, build.request)

            if (!resp.ok) {
                throw await buildHttpError(resp, url)
            }

            return resp.json()
        },
    }
}

// =============================================================================
// Internal Backend URL Resolution
// =============================================================================

function getInternalBackendUrl(override?: string): string {
    if (override) return override
    throw new Error(
        'baseUrl is required for SSR client. Pass it via config.'
    )
}

// =============================================================================
// SSR Client Factory
// =============================================================================

/**
 * Check if cookies is a CookieGetter interface.
 */
function isCookieGetter(cookies: SSRCookies): cookies is CookieGetter {
    return typeof (cookies as CookieGetter).get === 'function'
}

/**
 * Extract CSRF token and cookie header from SSRCookies.
 */
function extractCookies(cookies: SSRCookies): { csrf: string; cookieHeader: string } {
    if (isCookieGetter(cookies)) {
        return {
            csrf: cookies.get(getCsrfCookieName())?.value ?? '',
            cookieHeader: cookies.getAll().map(c => `${c.name}=${c.value}`).join('; ')
        }
    }
    return cookies
}

/**
 * Create a server-side HTTP client for Django.
 * Used in SSR contexts (Next.js server components, server actions, etc.)
 *
 * @param config - SSR client configuration with cookies
 * @returns MizanHTTPClient
 *
 * @example
 * // Next.js server component
 * import { cookies } from 'next/headers'
 *
 * const client = createMizanSSRClient({ cookies: await cookies() })
 */
// Re-export auth types for non-React usage
export type {
    BaseUser,
    AuthDetails,
    AuthRoutes,
    JWTTokens,
    JWTConfig,
    JWTState,
} from './types'

export function createMizanSSRClient(config: SSRClientConfig): MizanHTTPClient {
    const baseUrl = getInternalBackendUrl(config.baseUrl)
    const { csrf, cookieHeader } = extractCookies(config.cookies)

    return {
        request: async (method, path, data?, headers?) => {
            const url = new URL(path, baseUrl)

            const requestHeaders: Record<string, string> = {
                'Accept': 'application/json',
                [getCsrfHeaderName()]: csrf,
                'Cookie': cookieHeader,
                ...headers,
            }

            let body: BodyInit | undefined
            if (data && !['GET', 'HEAD'].includes(method.toUpperCase())) {
                if (data instanceof FormData) {
                    body = data
                } else {
                    body = JSON.stringify(data)
                    requestHeaders['Content-Type'] = 'application/json'
                }
            }

            return fetch(url, {
                method: method.toUpperCase(),
                headers: requestHeaders,
                body,
                credentials: 'include',
                cache: 'no-store',
            })
        },

        json: async <T>(method: string, path: string, data?: unknown, headers?: Record<string, string>): Promise<T> => {
            const url = new URL(path, baseUrl)

            const requestHeaders: Record<string, string> = {
                'Accept': 'application/json',
                [getCsrfHeaderName()]: csrf,
                'Cookie': cookieHeader,
                ...headers,
            }

            let body: BodyInit | undefined
            if (data && !['GET', 'HEAD'].includes(method.toUpperCase())) {
                if (data instanceof FormData) {
                    body = data
                } else {
                    body = JSON.stringify(data)
                    requestHeaders['Content-Type'] = 'application/json'
                }
            }

            const resp = await fetch(url, {
                method: method.toUpperCase(),
                headers: requestHeaders,
                body,
                credentials: 'include',
                cache: 'no-store',
            })

            if (!resp.ok) {
                throw await buildHttpError(resp, url)
            }

            const contentType = resp.headers.get('content-type') ?? ''
            if (!contentType.includes('application/json')) {
                throw new Error(`Expected JSON response but got ${contentType}`)
            }

            return resp.json()
        },
    }
}

// =============================================================================
// SSR Session Initialization
// =============================================================================

/**
 * Response from the session initialization endpoint.
 */
interface SessionInitResponse {
    csrfToken: string
}

/**
 * Ensure a Django session exists before making SSR requests.
 *
 * On first visit, the user has no cookies. This function pings Django to
 * establish a session and get a CSRF token, which can then be used for
 * subsequent hydration requests in the same SSR request chain.
 *
 * Note: Browser cookie forwarding is handled by Next.js middleware, not this
 * function. This function only ensures cookies exist for SSR data fetching.
 *
 * @param config - SSR client configuration with cookies
 * @returns Object with csrf token and cookie header for use in SSR requests
 *
 * @example
 * // In layout.tsx
 * const cookieStore = await cookies()
 * const session = await ensureMizanSession({ cookies: cookieStore })
 * const client = createMizanSSRClient({
 *     cookies: { csrf: session.csrf, cookieHeader: session.cookieHeader }
 * })
 */
export async function ensureMizanSession(config: SSRClientConfig): Promise<{
    csrf: string
    cookieHeader: string
}> {
    const baseUrl = getInternalBackendUrl(config.baseUrl)
    const { csrf: existingCsrf, cookieHeader: existingCookies } = extractCookies(config.cookies)

    // If we already have a CSRF token, just return existing cookies
    if (existingCsrf) {
        return { csrf: existingCsrf, cookieHeader: existingCookies }
    }

    // No CSRF token - need to initialize session
    const url = new URL('/api/mizan/session/', baseUrl)
    const resp = await fetch(url, {
        method: 'GET',
        headers: {
            'Accept': 'application/json',
            'Cookie': existingCookies,
        },
        credentials: 'include',
        cache: 'no-store',
    })

    if (!resp.ok) {
        console.error('[mizan] Failed to initialize session:', resp.status, resp.statusText)
        return { csrf: '', cookieHeader: existingCookies }
    }

    // Extract CSRF token from response body
    const data: SessionInitResponse = await resp.json()

    // Extract Set-Cookie headers to build updated cookie string for SSR chain
    const setCookieHeaders = resp.headers.getSetCookie?.() ?? []
    const newCookies = setCookieHeaders.map(c => c.split(';')[0]).join('; ')
    const combinedCookies = existingCookies
        ? `${existingCookies}; ${newCookies}`
        : newCookies

    return {
        csrf: data.csrfToken,
        cookieHeader: combinedCookies,
    }
}

// =============================================================================
// Server Function HTTP Call
// =============================================================================

// Re-export error types from the canonical location
export type { FunctionErrorResponse } from '../errors'
import { MizanError, type FunctionErrorResponse } from '../errors'

/**
 * Success response from a server function
 */
export interface FunctionSuccessResponse<T> {
    result: T
    invalidate?: Array<string | { context: string; params: Record<string, any> }>
}

/**
 * Union type for server function responses
 */
export type FunctionResponse<T> = FunctionSuccessResponse<T> | FunctionErrorResponse

