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

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

interface Node<T> {
    handler: Option<T>;
    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;

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

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

    setDynamicChild(paramName: string, handler?: T): DynamicNode<T> {
        const child = new DynamicNode(paramName, handler);
        this.dynamicChild = some(child);
        return child;
    }

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

    addChild(
        segment: string,
        wildcardSymbol: string,
        paramPrefixSymbol: string,
        handler?: T,
    ): Node<T> {
        if (segment === wildcardSymbol) {
            return this.setWildcardNode(handler);
        }
        if (segment.startsWith(paramPrefixSymbol)) {
            const paramName = segment.slice(paramPrefixSymbol.length);
            return this.setDynamicChild(paramName, handler);
        }
        return this.addStaticChild(segment, handler);
    }

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

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

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

    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(
        public readonly paramName: string,
        handler?: T,
    ) {
        super(handler);
    }

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

class WildcardNode<T> implements Node<T> {
    public handler: Option<T>;

    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 WildcardNode.");
    }

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

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

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

// Using Node<T> as the unified type for tree nodes.
type TreeNode<T> = Node<T>;

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

    constructor(
        handler?: T,
        private readonly wildcardSymbol: string = DEFAULT_WILDCARD_SYMBOL,
        private readonly paramPrefixSymbol: 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);
        let current: TreeNode<T> = this.root;

        for (const segment of segments) {
            current = current
                .getChild(segment)
                .unwrapOrElse(() =>
                    current.addChild(
                        segment,
                        this.wildcardSymbol,
                        this.paramPrefixSymbol,
                    )
                );

            if (current.isWildcardNode()) break;
        }

        current.handler = some(handler);
    }

    public find(path: string): Option<RouteMatch<T>> {
        const segments = this.splitPath(path);
        const params: Record<string, string> = {};
        let current: TreeNode<T> = this.root;
        let i = 0;

        for (; i < segments.length; i++) {
            const segment = segments[i];
            if (current.isWildcardNode()) break;

            const nextNode = current.getChild(segment).ifSome((child) => {
                if (child.isDynamicNode()) {
                    params[child.paramName] = segment;
                }
                current = child;
            });

            if (nextNode.isNone()) return none;
        }

        if (current.isWildcardNode()) {
            const rest = segments.slice(i - 1);
            if (rest.length > 0) {
                params["restOfThePath"] = rest.join(this.pathSeparator);
            }
        }

        return current.handler.map((value) => ({ value, params }));
    }

    public getHandler(path: string): Option<T> {
        const segments = this.splitPath(path);
        let current: TreeNode<T> = this.root;

        for (const segment of segments) {
            if (current.isWildcardNode()) break;

            const child = current.getChild(segment).ifSome((child) => {
                current = child;
            });

            if (child.isNone()) return none;
        }

        return current.handler;
    }

    private splitPath(path: string): string[] {
        const trimmed = path.trim().replace(/^\/+/, "").replace(/\/+$/, "");
        return trimmed ? trimmed.split(this.pathSeparator) : [];
    }
}

export type Params = Record<string, string>;

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