import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts";

const DEFAULT_WILDCARD = "*";
const DEFAULT_PARAM_PREFIX = ":";
const DEFAULT_PATH_SEPARATOR = "/";

export type Params = Record<string, string>;

interface RouteMatch<T> {
    value: T;
    params: Params;
}

interface Node<T> {
    handler: Option<T>;
    paramNames: string[];
    addChild(
        segment: string,
        wildcardSymbol: string,
        paramPrefixSymbol: string,
        handler?: T,
    ): Node<T>;
    getChild(segment: string): Option<Node<T>>;
    isDynamicNode(): this is DynamicNode<T>;
    isWildcardNode(): this is WildcardNode<T>;
}

class StaticNode<T> implements Node<T> {
    protected staticChildren = new Map<string, StaticNode<T>>();
    protected dynamicChild: Option<DynamicNode<T>> = none;
    protected wildcardChild: Option<WildcardNode<T>> = none;
    public handler: Option<T> = none;
    public paramNames: string[] = [];

    constructor(handler?: T) {
        this.handler = fromNullableVal(handler);
    }

    private addStaticChild(segment: string, handler?: T): StaticNode<T> {
        const child = new StaticNode(handler);
        this.staticChildren.set(segment, child);
        return child;
    }

    private createDynamicChild(handler?: T): DynamicNode<T> {
        const child = new DynamicNode(handler);
        this.dynamicChild = some(child);
        return child;
    }

    private createWildcardNode(handler?: T): WildcardNode<T> {
        const child = new WildcardNode(handler);
        this.wildcardChild = some(child);
        return child;
    }

    public addChild(
        segment: string,
        wildcardSymbol: string,
        paramPrefixSymbol: string,
        handler?: T,
    ): Node<T> {
        if (segment === wildcardSymbol) {
            return this.createWildcardNode(handler);
        }
        if (segment.startsWith(paramPrefixSymbol)) {
            return this.createDynamicChild(handler);
        }
        return this.addStaticChild(segment, handler);
    }

    private getStaticChild(segment: string): Option<StaticNode<T>> {
        return fromNullableVal(this.staticChildren.get(segment));
    }

    public getDynamicChild(): Option<DynamicNode<T>> {
        return this.dynamicChild;
    }

    public getWildcardChild(): Option<WildcardNode<T>> {
        return this.wildcardChild;
    }

    public getChild(segment: string): Option<Node<T>> {
        return this.getStaticChild(segment)
            .orElse(() => this.getWildcardChild())
            .orElse(() => this.getDynamicChild());
    }

    public isDynamicNode(): this is DynamicNode<T> {
        return false;
    }

    public isWildcardNode(): this is WildcardNode<T> {
        return false;
    }
}

class DynamicNode<T> extends StaticNode<T> implements Node<T> {
    constructor(
        handler?: T,
    ) {
        super(handler);
    }

    public override isDynamicNode(): this is DynamicNode<T> {
        return true;
    }
}

class WildcardNode<T> implements Node<T> {
    public handler: Option<T>;
    public paramNames: string[] = [];

    constructor(handler?: T) {
        this.handler = fromNullableVal(handler);
    }

    // Override to prevent adding children to a wildcard node
    public addChild(): Node<T> {
        throw new Error("Cannot add child to a wildcard (catch-all) node.");
    }

    public getChild(): Option<Node<T>> {
        return none;
    }

    public isWildcardNode(): this is WildcardNode<T> {
        return true;
    }

    public isDynamicNode(): this is DynamicNode<T> {
        return false;
    }
}

export class RouterTree<T> {
    public readonly root: StaticNode<T>;

    constructor(
        handler?: T,
        private readonly wildcardSymbol: string = DEFAULT_WILDCARD,
        private readonly paramPrefix: string = DEFAULT_PARAM_PREFIX,
        private readonly pathSeparator: string = DEFAULT_PATH_SEPARATOR,
    ) {
        this.root = new StaticNode(handler);
    }

    public add(path: string, handler: T): void {
        const segments = this.splitPath(path);
        const paramNames: string[] = this.extractParamNames(segments);
        const node: Node<T> = this.traverseOrCreate(segments);

        node.paramNames = node.isWildcardNode()
            ? [...paramNames, "restOfThePath"]
            : paramNames;
        node.handler = some(handler);
    }

    public find(path: string): Option<RouteMatch<T>> {
        return this.traverse(path).andThen(({ node, paramValues }) => {
            const params: Params = {};
            for (
                let i = 0;
                i < Math.min(paramValues.length, node.paramNames.length);
                i++
            ) {
                params[node.paramNames[i]] = paramValues[i];
            }
            return node.handler.map((handler) => ({ value: handler, params }));
        });
    }

    public getHandler(path: string): Option<T> {
        return this.traverse(path).andThen(({ node }) => node.handler);
    }

    private traverseOrCreate(segments: string[]): Node<T> {
        let node: Node<T> = this.root;
        for (const segment of segments) {
            if (node.isWildcardNode()) break;
            node = node.getChild(segment).unwrapOrElse(() =>
                node.addChild(segment, this.wildcardSymbol, this.paramPrefix)
            );
        }
        return node;
    }

    private traverse(
        path: string,
    ): Option<{ node: Node<T>; paramValues: string[] }> {
        const segments = this.splitPath(path);
        const paramValues: string[] = [];
        let node: Node<T> = this.root;

        for (let i = 0; i < segments.length; i++) {
            if (node.isWildcardNode()) {
                const remaining = segments.slice(i).join(this.pathSeparator);
                if (remaining) paramValues.push(remaining);
                return some({ node, paramValues });
            }

            const childOpt = node.getChild(segments[i]);
            if (childOpt.isNone()) return none;

            node = childOpt.unwrap();
            if (node.isDynamicNode()) {
                paramValues.push(segments[i]);
            }
        }

        return some({ node, paramValues });
    }

    private splitPath(path: string): string[] {
        return path
            .trim()
            .split(this.pathSeparator)
            .filter((segment) => segment.length > 0);
    }

    public extractParamNames(segments: string[]): string[] {
        return segments.filter((segment) =>
            segment.startsWith(this.paramPrefix)
        ).map((segment) => this.removeParamPrefix(segment));
    }

    public removeParamPrefix(segment: string): string {
        return segment.slice(this.paramPrefix.length);
    }
}
