interface TtyOptions {
    variables?: Record<string, string>;
    user: string;
    dir: string;
    banner: string;
    commands: {
        name: string;
        args: string;
    }[];
}
export const wait = (time: number) => new Promise((s) => setTimeout(s, time));

/**
 * @copyright Martin Flogaus 2026
 */
export class Tty {
    constructor(
        public element: HTMLDivElement,
        private options: Partial<TtyOptions> = {},
    ) {
        element.removeEventListener("click", this.onTerminalClick.bind(this));
        element.addEventListener("click", this.onTerminalClick.bind(this));

        element.dataset.tty = "true";

        this.variables = this.options.variables || this.variables;
        this.currentDir = this.options.dir || this.currentDir;
        this.currentUser = this.options.user || this.currentUser;

        this.init();
    }

    isRunningACommand: boolean;
    initializing: boolean;
    async init() {
        if (this.isRunningACommand || this.initializing) return;
        this.initializing = true;
        this.autofocus = false;
        this.element.innerHTML = `<div class="command-block"></div>`;

        if (this.options.banner) {
            this.options.banner.split("\n").map((line) => this.print(line));
        }
        await wait(500);

        this.newLine();

        for (const command of this.options.commands) {
            let value = command.name + (command.args ? " " + command.args : "");

            const splitValue = value.split("");
            while (splitValue.length) {
                this.currentInput.innerText += splitValue.shift();
                await wait(150);
            }
            await this.execCommand(command.name, ...(command.args?.split(" ") || [""]));
            await wait(1000);
        }
        this.autofocus = true;
        this.initializing = false;
    }

    autofocus = false;
    history = [""];
    variables = {};
    historyIndex = 0;
    currentDir = "~";
    currentUser = "root";
    onTerminalClick() {
        this.element.onkeyup = this.onInput.bind(this);
        this.historyIndex = this.history.length - 1;
        this.currentDir = (this.currentCommandBlock.querySelector(".text-info") as HTMLSpanElement).innerText;
        this.currentUser = (this.currentCommandBlock.querySelector(".text-success") as HTMLSpanElement).innerText;

        this.currentInput.onfocus = () => {
            this.setEndOfContenteditable(this.currentInput);
        };
        if (this.autofocus) this.currentInput.focus();
    }

    setEndOfContenteditable(contentEditableElement: HTMLDivElement) {
        if (document.createRange) {
            const range = document.createRange();
            range.selectNodeContents(contentEditableElement);
            range.collapse(false);
            const selection = window.getSelection();
            selection.removeAllRanges();
            selection.addRange(range);
        }
    }

    get currentInput(): HTMLDivElement {
        return this.currentCommandBlock.querySelector(".command-name");
    }

    get currentCommandBlock(): HTMLDivElement {
        return this.element.querySelector(".command-block:last-child");
    }

    traverse(path: string) {
        // 1. Standardize the path to absolute format for the lookup
        // (Translating ~ to the actual object structure)
        let cleanPath = path.replace(/^~/, "/home/" + this.currentUser);

        // 2. Split into segments and remove empty strings
        // e.g., "/home/ubuntu" -> ["home", "ubuntu"]
        const segments = cleanPath.split("/").filter((segment) => segment.length > 0);

        // 3. Walk the object tree
        let current = window.Terminal.fs;

        for (const segment of segments) {
            if (current && typeof current === "object" && segment in current) {
                current = current[segment];
            } else {
                throw new Error(`Directory not found: ${segment}`);
            }
        }

        // 4. Ensure we actually landed on a directory (object), not a file (string/null)
        if (current === null) {
            throw new Error("Not existing");
        }
        if(typeof current !== "object") {
            const error = new Error("Not directory");
            // @ts-ignore
            error.current = current
            throw error
        }

        return current;
    }
    async onInput(event: KeyboardEvent & { target: HTMLDivElement }): Promise<void> {
        if (event.key === "Tab") event.preventDefault();

        if (event.key === "Enter") {
            event.target.contentEditable = "false";
            const commandLine = event.target.innerText.split(" ");
            this.history.push(event.target.innerText);
            await this.execCommand(commandLine.shift(), ...commandLine);
        }
        if (event.key === "ArrowUp") {
            if ((event.target.innerText = this.history[this.historyIndex] || "")) {
                this.historyIndex--;
                this.setEndOfContenteditable(this.currentInput);
            }
        }
        if (event.key === "ArrowDown") {
            if ((event.target.innerText = this.history[this.historyIndex] || "")) {
                this.historyIndex++;
                this.setEndOfContenteditable(this.currentInput);
            }
        }
    }

    print(text: string) {
        const block = document.createElement("div");
        block.classList.add("command-result");
        block.innerHTML = text;
        this.currentCommandBlock.appendChild(block);
    }

    newLine() {
        const newBlock = document.createElement("div");
        newBlock.classList.add("command-block");

        let dir = this.currentDir.replace("/home/" + this.currentUser, "~");

        this.element.appendChild(newBlock);
        newBlock.innerHTML = `
        <div class="d-flex flex-row">
        <span class="text-success">${this.currentUser}</span>:<span class="text-info">${dir}</span>$ <span class="command-name flex-fill" contenteditable="plaintext-only"></span>
        </div>
        `;
    }

    private async execCommand(name: string, ...args: string[]) {
        this.isRunningACommand = true;
        if (name.length) {
            if (!window.Terminal.commands.has(name)) {
                if (name.includes("=")) {
                    const [varName, varValue] = (name + args.join(" ")).split("=");
                    this.variables["$" + varName] = varValue.replace(/^['"](.*)['"]$/g, "$1");
                } else this.print(`${name}: command not found`);
            } else {
                const result = await window.Terminal.commands.get(name)?.call(this, ...args);
                if (isNaN(result)) {
                    this.print(result);
                } else if (result) {
                    this.print(`Command ${name} exited with errorcode ${result}`);
                }
            }
        }
        this.newLine();
        this.onTerminalClick();
        this.isRunningACommand = false;
    }
}
