// retoor <retoor@molodetz.nl>

import { Http } from "./Http.js";
import { Toast } from "./Toast.js";
import { CodeMirrorModes } from "./codemirrorModes.js";

export class ProjectFiles {
    constructor(root) {
        this.root = root;
        this.slug = root.dataset.slug;
        this.isOwner = root.dataset.owner === "1";
        this.base = `/projects/${this.slug}/files`;
        this.files = [];
        this.editor = null;
        this.currentPath = null;
        this.currentDir = "";
        this.selected = new Set();
        this.order = [];
        this.anchor = null;
        this.dragPaths = null;
        this.tree = root.querySelector("#pf-tree");
        this.preview = root.querySelector("#pf-preview");
        this.empty = root.querySelector("#pf-empty");
        this.emptyMsg = root.querySelector(".pf-empty-msg");
        this.currentLabel = root.querySelector("#pf-current");
        this.editorWrap = root.querySelector("#pf-editor-wrap");
        this.uploadWidget = root.querySelector("#pf-upload-widget");
        this.status = root.querySelector("#pf-status");
    }

    notify(message) {
        if (this.status) Toast.flash(this.status, message, 3000, "");
    }

    init() {
        const data = this.root.querySelector("#pf-data");
        this.files = data ? JSON.parse(data.textContent || "[]") : [];
        this.initEditor();
        this.bindToolbar();
        this.bindTree();
        this.renderTree();
        this.openDefault();
    }

    firstFilePath() {
        for (const path of this.order) {
            const node = this.files.find((file) => file.path === path);
            if (node && node.type === "file") return path;
        }
        return null;
    }

    openDefault() {
        const files = this.files.filter((file) => file.type === "file");
        const readme = files.find((file) => file.name.toLowerCase() === "readme.md")
            || files.find((file) => file.name.toLowerCase().startsWith("readme."));
        const path = readme ? readme.path : this.firstFilePath();
        if (!path) {
            this.showEmpty(this.isOwner ? "Create a file to get started." : "No files yet.");
            return;
        }
        this.selected = new Set([path]);
        this.anchor = path;
        this.applySelection();
        this.markActive(path);
        this.currentDir = this.dirOf(path);
        this.openFile(path);
    }

    markActive(path) {
        this.root.querySelectorAll(".pf-node-row.active").forEach((el) => el.classList.remove("active"));
        const row = this.root.querySelector(`.pf-node-row[data-path="${CSS.escape(path)}"]`);
        if (row) row.classList.add("active");
    }

    initEditor() {
        if (typeof CodeMirror === "undefined") return;
        const textarea = this.root.querySelector("#project-file-editor");
        if (!textarea) return;
        this.editor = CodeMirror.fromTextArea(textarea, {
            lineNumbers: true,
            mode: "plaintext",
            theme: "monokai",
            indentUnit: 4,
            tabSize: 4,
            lineWrapping: true,
            readOnly: !this.isOwner,
            extraKeys: { "Ctrl-S": () => this.save() },
        });
        this.editor.setSize(null, "100%");
    }

    bindToolbar() {
        const on = (id, handler) => {
            const el = this.root.querySelector(id);
            if (el) el.addEventListener("click", (e) => { e.preventDefault(); handler(); });
        };
        on("#pf-new-file", () => this.newFile());
        on("#pf-new-folder", () => this.newFolder());
        on("#pf-upload", () => this.uploadTo(this.currentDir));
        on("#pf-save", () => this.save());
        on("#pf-rename", () => this.rename());
        on("#pf-delete", () => this.remove());
        if (this.uploadWidget) {
            this.uploadWidget.addEventListener("dp-upload:done", () => this.refresh());
            this.uploadWidget.addEventListener("dp-upload:error", (e) => this.notify(e.detail.message));
        }
    }

    bindTree() {
        this.tree.addEventListener("click", (e) => {
            if (e.target === this.tree) {
                this.selected.clear();
                this.applySelection();
            }
        });
        if (!this.isOwner) return;
        if (window.app && window.app.contextMenu) {
            window.app.contextMenu.attach(this.tree, (e) => this.buildMenu(e));
        }
        this.tree.addEventListener("dragover", (e) => {
            if (!this.dragPaths) return;
            e.preventDefault();
            e.dataTransfer.dropEffect = "move";
            this.tree.classList.add("pf-root-drop");
        });
        this.tree.addEventListener("dragleave", (e) => {
            if (e.target === this.tree) this.tree.classList.remove("pf-root-drop");
        });
        this.tree.addEventListener("drop", (e) => {
            if (!this.dragPaths) return;
            e.preventDefault();
            this.tree.classList.remove("pf-root-drop");
            this.onDrop("");
        });
    }

    dirOf(path) {
        return path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
    }

    basename(path) {
        return path.split("/").pop();
    }

    joinDir(dir, name) {
        const clean = name.replace(/^\/+/, "");
        return dir ? `${dir}/${clean}` : clean;
    }

    buildTree() {
        const nodes = new Map();
        this.files.forEach((file) => nodes.set(file.path, { ...file, children: [] }));
        const roots = [];
        this.files.forEach((file) => {
            const node = nodes.get(file.path);
            const parent = file.path.includes("/") ? nodes.get(file.path.slice(0, file.path.lastIndexOf("/"))) : null;
            (parent ? parent.children : roots).push(node);
        });
        const sort = (list) => {
            list.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1));
            list.forEach((node) => sort(node.children));
        };
        sort(roots);
        return roots;
    }

    renderTree() {
        this.order = [];
        const known = new Set(this.files.map((f) => f.path));
        this.selected = new Set([...this.selected].filter((p) => known.has(p)));
        this.tree.textContent = "";
        this.tree.appendChild(this.renderRootRow());
        const roots = this.buildTree();
        if (!roots.length) {
            const hint = document.createElement("div");
            hint.className = "pf-tree-empty";
            hint.textContent = this.isOwner ? "Empty project. Create a file or folder." : "No files yet.";
            this.tree.appendChild(hint);
            return;
        }
        this.tree.appendChild(this.renderNodes(roots));
        this.applySelection();
    }

    renderRootRow() {
        const row = document.createElement("button");
        row.type = "button";
        row.className = "pf-node-row pf-root-row";
        row.dataset.path = "";
        row.dataset.type = "dir";
        row.draggable = false;
        row.innerHTML = '<span class="pf-icon">&#x1F4E6;</span><span class="pf-label">project root</span>';
        row.addEventListener("click", () => {
            this.selected.clear();
            this.applySelection();
            this.root.querySelectorAll(".pf-node-row.active").forEach((el) => el.classList.remove("active"));
            row.classList.add("active");
            this.currentPath = null;
            this.currentDir = "";
            this.showEmpty("Project root");
        });
        if (this.isOwner) {
            row.addEventListener("dragover", (e) => {
                if (!this.dragPaths) return;
                e.preventDefault();
                e.dataTransfer.dropEffect = "move";
                row.classList.add("pf-dragover");
            });
            row.addEventListener("dragleave", () => row.classList.remove("pf-dragover"));
            row.addEventListener("drop", (e) => {
                if (!this.dragPaths) return;
                e.preventDefault();
                e.stopPropagation();
                row.classList.remove("pf-dragover");
                this.onDrop("");
            });
        }
        return row;
    }

    renderNodes(list) {
        const ul = document.createElement("ul");
        ul.className = "pf-tree-list";
        list.forEach((node) => {
            this.order.push(node.path);
            const li = document.createElement("li");
            li.className = `pf-node pf-${node.type}`;
            const row = document.createElement("button");
            row.type = "button";
            row.className = "pf-node-row";
            row.dataset.path = node.path;
            row.dataset.type = node.type;
            row.draggable = this.isOwner;
            row.innerHTML = `<span class="pf-icon">${node.type === "dir" ? "&#x1F4C1;" : "&#x1F4C4;"}</span><span class="pf-label"></span>`;
            row.querySelector(".pf-label").textContent = node.name;
            row.addEventListener("click", (e) => this.onRowClick(node, li, e));
            if (this.isOwner) this.bindDrag(row, node);
            li.appendChild(row);
            if (node.type === "dir") {
                const child = this.renderNodes(node.children);
                child.classList.add("pf-children");
                li.appendChild(child);
            }
            ul.appendChild(li);
        });
        return ul;
    }

    bindDrag(row, node) {
        row.addEventListener("dragstart", (e) => {
            const paths = this.selected.has(node.path) && this.selected.size ? Array.from(this.selected) : [node.path];
            this.dragPaths = paths;
            e.dataTransfer.effectAllowed = "move";
            e.dataTransfer.setData("text/plain", paths.join("\n"));
            row.classList.add("pf-dragging");
        });
        row.addEventListener("dragend", () => {
            row.classList.remove("pf-dragging");
            this.dragPaths = null;
            this.root.querySelectorAll(".pf-dragover").forEach((el) => el.classList.remove("pf-dragover"));
            this.tree.classList.remove("pf-root-drop");
        });
        if (node.type !== "dir") return;
        row.addEventListener("dragover", (e) => {
            if (!this.dragPaths) return;
            e.preventDefault();
            e.dataTransfer.dropEffect = "move";
            row.classList.add("pf-dragover");
        });
        row.addEventListener("dragleave", () => row.classList.remove("pf-dragover"));
        row.addEventListener("drop", (e) => {
            if (!this.dragPaths) return;
            e.preventDefault();
            e.stopPropagation();
            row.classList.remove("pf-dragover");
            this.onDrop(node.path);
        });
    }

    onDrop(targetDir) {
        const paths = this.dragPaths || [];
        this.dragPaths = null;
        if (paths.length) this.moveInto(targetDir, paths);
    }

    onRowClick(node, li, e) {
        if (e.shiftKey && this.anchor) {
            this.selectRange(this.anchor, node.path);
            return;
        }
        if (e.ctrlKey || e.metaKey) {
            if (this.selected.has(node.path)) this.selected.delete(node.path);
            else this.selected.add(node.path);
            this.anchor = node.path;
            this.applySelection();
            return;
        }
        this.selected = new Set([node.path]);
        this.anchor = node.path;
        this.applySelection();
        this.activate(node, li);
    }

    activate(node, li) {
        this.root.querySelectorAll(".pf-node-row.active").forEach((el) => el.classList.remove("active"));
        const row = li.querySelector(".pf-node-row");
        if (row) row.classList.add("active");
        if (node.type === "dir") {
            li.classList.toggle("collapsed");
            this.currentDir = node.path;
            return;
        }
        this.currentDir = this.dirOf(node.path);
        this.openFile(node.path);
    }

    selectRange(fromPath, toPath) {
        const i = this.order.indexOf(fromPath);
        const j = this.order.indexOf(toPath);
        if (i < 0 || j < 0) return;
        const [a, b] = i <= j ? [i, j] : [j, i];
        this.selected = new Set(this.order.slice(a, b + 1));
        this.applySelection();
    }

    applySelection() {
        this.root.querySelectorAll(".pf-node-row").forEach((row) => {
            row.classList.toggle("pf-selected", this.selected.has(row.dataset.path));
        });
        if (this.status && this.selected.size > 1) Toast.flash(this.status, `${this.selected.size} selected`, 1500, "");
    }

    buildMenu(e) {
        if (!this.isOwner) return [];
        const rowEl = e.target.closest(".pf-node-row");
        if (!rowEl) {
            return [
                { label: "New file", icon: "&#x1F4C4;", onSelect: () => this.newFile("") },
                { label: "New folder", icon: "&#x1F4C1;", onSelect: () => this.newFolder("") },
                { label: "Upload here", icon: "&#x2B06;", onSelect: () => this.uploadTo("") },
            ];
        }
        const path = rowEl.dataset.path;
        const type = rowEl.dataset.type;
        if (this.selected.size > 1 && this.selected.has(path)) {
            const count = this.selected.size;
            return [
                { label: `Move ${count} items to...`, icon: "&#x27A1;", onSelect: () => this.promptMove(Array.from(this.selected)) },
                { separator: true },
                { label: `Delete ${count} items`, icon: "&#x1F5D1;", danger: true, onSelect: () => this.remove(path) },
            ];
        }
        const dir = type === "dir" ? path : this.dirOf(path);
        const items = [];
        if (type === "file") {
            items.push({ label: "Open", icon: "&#x1F4C4;", onSelect: () => this.openFile(path) });
        } else {
            items.push({ label: "New file here", icon: "&#x1F4C4;", onSelect: () => this.newFile(dir) });
            items.push({ label: "New folder here", icon: "&#x1F4C1;", onSelect: () => this.newFolder(dir) });
            items.push({ label: "Upload here", icon: "&#x2B06;", onSelect: () => this.uploadTo(dir) });
        }
        items.push({ separator: true });
        items.push({ label: "Download as zip", icon: "&#x1F4E6;", onSelect: () => window.app.zipDownloader.start(`${this.base}/zip?path=${encodeURIComponent(path)}`) });
        items.push({ label: "Rename / move", icon: "&#x270F;", onSelect: () => this.rename(path) });
        items.push({ label: "Delete", icon: "&#x1F5D1;", danger: true, onSelect: () => this.remove(path) });
        return items;
    }

    async openFile(path) {
        try {
            const data = await Http.getJson(`${this.base}/raw?path=${encodeURIComponent(path)}`);
            this.currentPath = path;
            this.currentLabel.textContent = path;
            this.currentLabel.hidden = false;
            if (data.is_binary) {
                this.showPreview(data);
            } else {
                this.showEditor(path, data.content || "");
            }
        } catch (err) {
            this.notify(err.message);
        }
    }

    showEditor(path, content) {
        this.empty.hidden = true;
        this.preview.hidden = true;
        this.editorWrap.hidden = false;
        if (this.editor) {
            this.editor.setOption("mode", CodeMirrorModes.modeForFilename(path));
            this.editor.setValue(content);
            setTimeout(() => this.editor.refresh(), 50);
        }
    }

    showPreview(data) {
        this.empty.hidden = true;
        this.editorWrap.hidden = true;
        this.preview.hidden = false;
        this.preview.textContent = "";
        const mime = data.mime_type || "";
        if (mime.startsWith("image/")) {
            const img = document.createElement("img");
            img.src = data.url;
            img.alt = data.name;
            this.preview.appendChild(img);
        } else if (mime.startsWith("video/")) {
            const video = document.createElement("video");
            video.src = data.url;
            video.controls = true;
            video.preload = "metadata";
            this.preview.appendChild(video);
        } else {
            const link = document.createElement("a");
            link.href = data.url;
            link.textContent = `Download ${data.name}`;
            link.className = "pf-download";
            link.setAttribute("download", data.name);
            this.preview.appendChild(link);
        }
    }

    showEmpty(message) {
        this.editorWrap.hidden = true;
        this.preview.hidden = true;
        this.empty.hidden = false;
        if (this.emptyMsg) this.emptyMsg.textContent = message || "Select a file to view or edit.";
        this.currentLabel.textContent = "";
        this.currentLabel.hidden = true;
    }

    post(action, body) {
        return Http.send(`${this.base}/${action}`, body);
    }

    async refresh(openPath) {
        const data = await Http.getJson(this.base);
        this.files = data.files || [];
        this.renderTree();
        if (openPath) this.openFile(openPath);
    }

    async promptInput(options) {
        const dialog = window.app && window.app.dialog;
        if (dialog) return dialog.prompt(options);
        return window.prompt(options.message || options.label || options.title || "", options.value || "");
    }

    async confirmAction(options) {
        const dialog = window.app && window.app.dialog;
        if (dialog) return dialog.confirm(options);
        return window.confirm(options.message);
    }

    async newFile(baseDir) {
        const dir = baseDir !== undefined ? baseDir : this.currentDir;
        const name = await this.promptInput({
            title: "New file",
            label: dir ? `Inside ${dir}/` : "In project root",
            placeholder: "main.py",
            confirmLabel: "Create",
        });
        if (!name || !name.trim()) return;
        const path = this.joinDir(dir, name.trim());
        try {
            await this.post("write", { path, content: "" });
            await this.refresh(path);
        } catch (err) { this.notify(err.message); }
    }

    async newFolder(baseDir) {
        const dir = baseDir !== undefined ? baseDir : this.currentDir;
        const name = await this.promptInput({
            title: "New folder",
            label: dir ? `Inside ${dir}/` : "In project root",
            placeholder: "src",
            confirmLabel: "Create",
        });
        if (!name || !name.trim()) return;
        const path = this.joinDir(dir, name.trim());
        try {
            await this.post("mkdir", { path });
            this.currentDir = path;
            await this.refresh();
        } catch (err) { this.notify(err.message); }
    }

    uploadTo(dir) {
        if (!this.uploadWidget) return;
        this.uploadWidget.extraFields = { path: dir };
        this.uploadWidget.open();
    }

    async save() {
        if (!this.currentPath || !this.editor) return;
        try {
            await this.post("write", { path: this.currentPath, content: this.editor.getValue() });
            this.notify("Saved");
        } catch (err) { this.notify(err.message); }
    }

    async rename(target) {
        const from = target || this.currentPath || this.currentDir;
        if (!from) { this.notify("Select a file or folder first"); return; }
        const to = await this.promptInput({
            title: "Rename / move",
            label: "New path",
            value: from,
            confirmLabel: "Save",
        });
        if (!to || !to.trim() || to.trim() === from) return;
        try {
            await this.post("move", { from_path: from, to_path: to.trim() });
            await this.refresh(this.currentPath === from ? to.trim() : null);
        } catch (err) { this.notify(err.message); }
    }

    async remove(target) {
        const multi = this.selected.size > 1 && (!target || this.selected.has(target));
        const paths = multi ? Array.from(this.selected) : [target || this.currentPath || this.currentDir].filter(Boolean);
        if (!paths.length) { this.notify("Select a file or folder first"); return; }
        const message = paths.length > 1
            ? `Delete ${paths.length} items? This cannot be undone.`
            : `Delete "${paths[0]}"? This cannot be undone.`;
        const ok = await this.confirmAction({ title: "Delete", message, danger: true, confirmLabel: "Delete" });
        if (!ok) return;
        try {
            for (const path of paths) await this.post("delete", { path });
            this.selected.clear();
            this.currentPath = null;
            await this.refresh();
            this.openDefault();
        } catch (err) { this.notify(err.message); }
    }

    async promptMove(paths) {
        const to = await this.promptInput({
            title: `Move ${paths.length} items`,
            label: "Target directory (empty for project root)",
            placeholder: "src",
            confirmLabel: "Move",
        });
        if (to === null || to === undefined) return;
        await this.moveInto(to.trim().replace(/^\/+|\/+$/g, ""), paths);
    }

    async moveInto(targetDir, paths) {
        const valid = paths.filter((path) => {
            if (targetDir === path || targetDir.startsWith(`${path}/`)) return false;
            if (targetDir === this.dirOf(path)) return false;
            return true;
        });
        if (!valid.length) return;
        try {
            for (const path of valid) {
                const to = targetDir ? `${targetDir}/${this.basename(path)}` : this.basename(path);
                await this.post("move", { from_path: path, to_path: to });
            }
            this.selected.clear();
            await this.refresh();
        } catch (err) { this.notify(err.message); }
    }

}
