import { type Params } from "@lib/router.ts";
import { type ExtractRouteParams } from "@lib/router.ts";
import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts";
import { deleteCookie, getCookies, setCookie } from "@std/http/cookie";
import { type Cookie } from "@std/http/cookie";
import { Err, Ok, type Result, ResultFromJSON } from "@shared/utils/result.ts";

// https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html
const SECURITY_HEADERS: Headers = new Headers({
    "X-Frame-Option": "DENY",
    "X-Content-Type-Options": "nosniff",
    //"Referrer-Policy": "strict-origin-when-cross-origin",
    //"Cross-Origin-Opener-Policy": "same-origin",
    //"Cross-Origin-Resource-Policy": "same-site",
    "Permissions-Policy": "geolocation=(), camera=(), microphone=()",
    Server: "webserver",
    //"Content-Security-Policy":
    //    "default-src 'self'; script-src 'self' 'unsafe-inline'",
});
const HTML_CONTENT_TYPE: [string, string] = [
    "Content-Type",
    "text/html; charset=UTF-8",
];
const JSON_CONTENT_TYPE: [string, string] = [
    "Content-Type",
    "application/json; charset=utf-8",
];

function mergeHeaders(...headers: Headers[]): Headers {
    const mergedHeaders = new Headers();
    for (const _headers of headers) {
        for (const [key, value] of _headers.entries()) {
            mergedHeaders.set(key, value);
        }
    }
    return mergedHeaders;
}

export class Context<S extends string = string> {
    private _url?: URL;
    private _hostname?: string;
    private _port?: number;
    private _cookies?: Record<string, string>;
    private _responseHeaders: Headers = new Headers();
    public res: Response = new Response();

    constructor(
        public readonly req: Request,
        public readonly info: Deno.ServeHandlerInfo<Deno.Addr>,
        public readonly params: Params<ExtractRouteParams<S>>,
    ) {}

    get url(): URL {
        return this._url ?? (this._url = new URL(this.req.url));
    }

    get path(): ExtractPath<S> {
        return this.url.pathname as ExtractPath<S>;
    }

    get preferredType(): Option<"json" | "html"> {
        const headers = new Headers(this.req.headers);

        return fromNullableVal(headers.get("accept")).andThen(
            (types_header) => {
                const types = types_header.split(";")[0].trim().split(",");

                for (const type of types) {
                    if (type === "text/html") {
                        return some("html");
                    }
                    if (type === "application/json") {
                        return some("json");
                    }
                }
                return none;
            },
        );
    }

    get hostname(): Option<string> {
        if (this._hostname) return some(this._hostname);

        const remoteAddr = this.info.remoteAddr;

        if (remoteAddr.transport === "tcp" || remoteAddr.transport === "udp") {
            this._hostname = remoteAddr.hostname;
            return some(remoteAddr.hostname);
        }
        return none;
    }

    get port(): Option<number> {
        if (this._port) return some(this._port);

        const remoteAddr = this.info.remoteAddr;

        if (remoteAddr.transport === "tcp" || remoteAddr.transport === "udp") {
            this._port = remoteAddr.port;
            return some(remoteAddr.port);
        }
        return none;
    }

    public json(body?: object | string, init: ResponseInit = {}): Response {
        const headers = mergeHeaders(
            SECURITY_HEADERS,
            this._responseHeaders,
            new Headers(init.headers),
        );
        headers.set(...JSON_CONTENT_TYPE);
        let status = init.status || 200;

        let responseBody: BodyInit | null = null;
        if (typeof body === "string") {
            responseBody = body;
        } else if (body !== undefined) {
            try {
                responseBody = JSON.stringify(body);
            } catch (error) {
                console.error("Failed to serialize JSON body:", error);
                responseBody = JSON.stringify({
                    err: "Internal Server Error",
                });
                status = 500;
            }
        }

        return new Response(responseBody, {
            status,
            headers,
        });
    }

    public html(body?: BodyInit | null, init: ResponseInit = {}): Response {
        const headers = mergeHeaders(
            SECURITY_HEADERS,
            this._responseHeaders,
            new Headers(init.headers),
        );
        headers.set(...HTML_CONTENT_TYPE);
        const status = init.status ?? 200;

        return new Response(body ?? null, {
            status,
            headers,
        });
    }

    public redirect(url: string, permanent = false): Response {
        const headers = mergeHeaders(
            this._responseHeaders,
            new Headers({ location: url }),
        );

        return new Response(null, {
            status: permanent ? 301 : 302,
            headers,
        });
    }

    public cookies = (() => {
        const self = this;

        return {
            get(name: string): Option<string> {
                if (!self._cookies) {
                    self._cookies = getCookies(self.req.headers);
                }

                return fromNullableVal(self._cookies[name]);
            },

            set(cookie: Cookie) {
                setCookie(self._responseHeaders, cookie);
            },

            delete(name: string) {
                deleteCookie(self._responseHeaders, name);
            },
        };
    })();

    static setParams<S extends string>(
        ctx: Context<string>,
        params: Params<ExtractRouteParams<S>>,
    ): Context<S> {
        const newCtx = new Context(ctx.req, ctx.info, params);

        newCtx._url = ctx._url;
        newCtx._hostname = ctx._hostname;
        newCtx._port = ctx._port;
        newCtx._cookies = ctx._cookies;
        newCtx._responseHeaders = ctx._responseHeaders;

        return newCtx;
    }
}

type ExtractPath<S extends string> = S extends
    `${infer _Start}:${infer Param}/${infer Rest}` ? string
    : S extends `${infer _Start}/:${infer Param}` ? string
    : S extends `${infer _Start}*` ? string
    : S;
