import http from "http";
import {createReadStream, createWriteStream, existsSync} from "fs";
import {promises as fs} from "fs";
import path from "path";
import {fileURLToPath} from "url";
import crypto from "crypto";
import {pipeline} from "stream/promises";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const DATA_DIR = path.join(__dirname, "data", "projects");
const UPLOADS_DIR = path.join(__dirname, "data", "uploads");
const INDEX_FILE = path.join(DATA_DIR, "index.json");
const FRONTEND_DIST = path.join(__dirname, "..", "frontend", "dist");
const FRONTEND_ROOT = path.join(__dirname, "..", "frontend");
const PORT = parseInt(process.env.PORT, 10) || 8080;

const MIME_TYPES = {
    ".html": "text/html",
    ".js": "text/javascript",
    ".mjs": "text/javascript",
    ".css": "text/css",
    ".json": "application/json",
    ".svg": "image/svg+xml",
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".gif": "image/gif",
    ".ico": "image/x-icon",
    ".glb": "model/gltf-binary",
    ".gltf": "model/gltf+json",
    ".xkt": "application/octet-stream",
    ".las": "application/octet-stream",
    ".laz": "application/octet-stream",
    ".bim": "application/octet-stream",
    ".step": "application/octet-stream",
    ".stp": "application/octet-stream",
    ".wasm": "application/wasm",
    ".txt": "text/plain"
};

const CONVERTIBLE_MODEL_FORMATS = new Map([
    [".step", "step"],
    [".stp", "step"]
]);

const nowIso = () => new Date().toISOString();

const ensureStorage = async () => {
    await fs.mkdir(DATA_DIR, {recursive: true});
    await fs.mkdir(UPLOADS_DIR, {recursive: true});
    try {
        await fs.access(INDEX_FILE);
    } catch (err) {
        await fs.writeFile(INDEX_FILE, "[]", "utf8");
    }
};

const readIndex = async () => {
    try {
        const raw = await fs.readFile(INDEX_FILE, "utf8");
        const parsed = JSON.parse(raw);
        return Array.isArray(parsed) ? parsed : [];
    } catch (err) {
        console.error("[server] failed to read index.json", err);
        return [];
    }
};

const writeIndex = async (items) => {
    await fs.writeFile(INDEX_FILE, JSON.stringify(items, null, 2), "utf8");
};

const sendJSON = (res, status, payload) => {
    setCors(res);
    res.statusCode = status;
    res.setHeader("Content-Type", "application/json");
    res.end(JSON.stringify(payload));
};

const sendText = (res, status, message) => {
    setCors(res);
    res.statusCode = status;
    res.setHeader("Content-Type", "text/plain");
    res.end(message);
};

const setCors = (res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type");
};

const parseJSONBody = async (req) => {
    const chunks = [];
    for await (const chunk of req) {
        chunks.push(chunk);
    }
    if (!chunks.length) {
        return {};
    }
    const raw = Buffer.concat(chunks).toString("utf8");
    if (!raw.trim()) {
        return {};
    }
    try {
        return JSON.parse(raw);
    } catch (err) {
        throw new Error("Invalid JSON");
    }
};

const safeProjectId = (id) => {
    if (typeof id !== "string") {
        return null;
    }
    const trimmed = id.trim();
    if (!/^[-_a-zA-Z0-9]+$/.test(trimmed)) {
        return null;
    }
    return trimmed;
};

const STATIC_ROOTS = [
    {prefix: "/uploads/", dir: UPLOADS_DIR},
    {prefix: "/data/", dir: path.join(__dirname, "data")},
    {prefix: "/", dir: FRONTEND_DIST},
    {prefix: "/", dir: FRONTEND_ROOT}
];

const resolveStaticPath = async (pathname) => {
    for (const {prefix, dir} of STATIC_ROOTS) {
        if (!pathname.startsWith(prefix)) {
            continue;
        }
        const rel = pathname.slice(prefix.length);
        const resolved = path.resolve(dir, rel);
        if (!resolved.startsWith(dir)) {
            continue;
        }
        try {
            const stats = await fs.stat(resolved);
            if (stats.isFile()) {
                return resolved;
            }
        } catch (_) {
            // keep searching
        }
    }
    return null;
};

const serveStatic = async (req, res, url) => {
    const pathname = url.pathname || "/";
    const targetPath = await resolveStaticPath(pathname);
    if (!targetPath) {
        const shouldFallback = pathname === "/" || !path.extname(pathname);
        if (shouldFallback) {
            const fallbackIndex = await resolveStaticPath("/index.html");
            if (fallbackIndex) {
                return serveStatic(req, res, new URL("/index.html", url));
            }
        }
        sendText(res, 404, "Not Found");
        return;
    }
    const ext = path.extname(targetPath).toLowerCase();
    const mime = MIME_TYPES[ext] || "application/octet-stream";
    res.statusCode = 200;
    res.setHeader("Content-Type", mime);
    createReadStream(targetPath).pipe(res).on("error", (err) => {
        console.error("[server] failed to stream file", err);
        if (!res.headersSent) {
            sendText(res, 500, "Failed to read file");
        } else {
            res.destroy();
        }
    });
};

const decodeBase64 = (b64) => {
    try {
        return Buffer.from(b64, "base64");
    } catch (err) {
        return null;
    }
};

const sanitizeFilename = (name, fallback) => {
    if (!name || typeof name !== "string") {
        return fallback;
    }
    const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
    return safe || fallback;
};

const manifestPathForSource = (sourcePath) => path.join(path.dirname(sourcePath), `${path.basename(sourcePath)}.beam.json`);

const srcFromUploadPath = (uploadPath) => {
    const relative = path.relative(UPLOADS_DIR, uploadPath).replace(/\\/g, "/");
    return path.join("uploads", relative).replace(/\\/g, "/");
};

const resolveUploadSrc = (src) => {
    if (!src || typeof src !== "string") {
        return null;
    }
    let value = src.trim();
    if (!value) {
        return null;
    }
    try {
        if (/^https?:\/\//i.test(value)) {
            const parsed = new URL(value);
            value = parsed.pathname;
        }
    } catch (_) {
        return null;
    }
    value = value.replace(/^\/+/, "");
    if (!value.startsWith("uploads/")) {
        return null;
    }
    const relative = value.slice("uploads/".length);
    const resolved = path.resolve(UPLOADS_DIR, relative);
    if (!resolved.startsWith(UPLOADS_DIR)) {
        return null;
    }
    return resolved;
};

const readJSONFile = async (filePath) => {
    try {
        const raw = await fs.readFile(filePath, "utf8");
        return JSON.parse(raw);
    } catch (_) {
        return null;
    }
};

const writeJSONFile = async (filePath, payload) => {
    await fs.writeFile(filePath, JSON.stringify(payload, null, 2), "utf8");
};

const processUploads = async (project) => {
    if (!Array.isArray(project.models)) {
        return;
    }
    const projectDir = path.join(UPLOADS_DIR, project.id);
    await fs.mkdir(projectDir, {recursive: true});
    for (let i = 0; i < project.models.length; i++) {
        const model = project.models[i];
        if (!model || !model.srcData) {
            continue;
        }
        const data = decodeBase64(model.srcData);
        if (!data) {
            throw new Error("Invalid srcData for model upload");
        }
        const ext = path.extname(model.srcFilename || "");
        const baseName = sanitizeFilename(model.srcFilename || `${model.id || "model"}${ext || ".bin"}`, `${model.id || "model"}.bin`);
        const filePath = path.join(projectDir, baseName);
        await fs.writeFile(filePath, data);
        model.src = path.join("uploads", project.id, baseName).replace(/\\/g, "/");
        delete model.srcData;
        delete model.srcFilename;
        delete model.srcMime;
    }
};

const handleRawUpload = async (req, res, url) => {
    const filenameParam = url.searchParams.get("filename");
    const filenameHeader = req.headers["x-filename"];
    const rawName = filenameParam || (Array.isArray(filenameHeader) ? filenameHeader[0] : filenameHeader) || "upload.bin";
    const safeName = sanitizeFilename(rawName, "upload.bin");
    // if client passes projectId in query, group uploads per project
    const projectId = safeProjectId(url.searchParams.get("projectId"));
    const uploadId = projectId || (crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2));
    const dir = path.join(UPLOADS_DIR, uploadId);
    await fs.mkdir(dir, {recursive: true});
    const targetPath = path.join(dir, safeName);
    const writeStream = createWriteStream(targetPath);
    try {
        await pipeline(req, writeStream);
    } catch (err) {
        console.error("[server] upload failed", err);
        sendText(res, 500, "Upload failed");
        return;
    }
    const src = path.join("uploads", uploadId, safeName).replace(/\\/g, "/");
    const sourceFormat = CONVERTIBLE_MODEL_FORMATS.get(path.extname(safeName).toLowerCase());
    if (sourceFormat) {
        const conversion = {
            componentTreeRequired: true,
            message: "Original STEP uploaded. GLB/component tree is waiting for NodeDcBimConverter.",
            sourceFormat,
            sourceSrc: src,
            status: "conversion_required",
            targetFormat: "glb",
            updatedAt: nowIso()
        };
        await writeJSONFile(manifestPathForSource(targetPath), conversion);
        sendJSON(res, 201, {
            src,
            conversion
        });
        return;
    }
    sendJSON(res, 201, {src});
};

const handleConversionStatus = async (res, searchParams) => {
    const sourcePath = resolveUploadSrc(searchParams.get("src"));
    if (!sourcePath) {
        sendText(res, 400, "Invalid source path");
        return;
    }
    if (!existsSync(sourcePath)) {
        sendText(res, 404, "Source file not found");
        return;
    }

    const sourceFormat = CONVERTIBLE_MODEL_FORMATS.get(path.extname(sourcePath).toLowerCase());
    if (!sourceFormat) {
        sendJSON(res, 200, {
            sourceSrc: srcFromUploadPath(sourcePath),
            status: "ready"
        });
        return;
    }

    const manifest = await readJSONFile(manifestPathForSource(sourcePath));
    if (!manifest) {
        sendJSON(res, 200, {
            componentTreeRequired: true,
            message: "GLB/component tree is waiting for NodeDcBimConverter.",
            sourceFormat,
            sourceSrc: srcFromUploadPath(sourcePath),
            status: "conversion_required",
            targetFormat: "glb",
            updatedAt: nowIso()
        });
        return;
    }

    sendJSON(res, 200, manifest);
};

const buildProjectPayload = (body) => {
    const now = new Date().toISOString();
    const id = `proj_${crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2)}`;
    const name = typeof body.name === "string" ? body.name.trim() : "";
    if (!name) {
        throw new Error("Project name is required");
    }
    const type = body.type === "template" ? "template" : "project";
    const models = Array.isArray(body.models) ? body.models : [];
    const viewerState = body.viewerState && typeof body.viewerState === "object" ? body.viewerState : {};
    const design = body.design && typeof body.design === "object" ? body.design : {};
    const meta = body.meta && typeof body.meta === "object" ? body.meta : {description: "", tags: []};

    return {
        id,
        name,
        type,
        createdAt: now,
        updatedAt: now,
        ownerId: null,
        shares: [],
        models,
        viewerState,
        design,
        meta
    };
};

const buildUpdatedProject = (body, existing) => {
    const now = new Date().toISOString();
    const name = typeof body.name === "string" ? body.name.trim() : "";
    if (!name) {
        throw new Error("Project name is required");
    }
    const type = body.type === "template" ? "template" : (body.type === "project" ? "project" : (existing.type || "project"));
    const models = Array.isArray(body.models) ? body.models : (existing.models || []);
    const viewerState = body.viewerState && typeof body.viewerState === "object" ? body.viewerState : (existing.viewerState || {});
    const design = body.design && typeof body.design === "object" ? body.design : (existing.design || {});
    const meta = body.meta && typeof body.meta === "object" ? body.meta : (existing.meta || {description: "", tags: []});

    return {
        id: existing.id,
        name,
        type,
        createdAt: existing.createdAt || now,
        updatedAt: now,
        ownerId: existing.ownerId ?? null,
        shares: Array.isArray(existing.shares) ? existing.shares : [],
        models,
        viewerState,
        design,
        meta
    };
};

const handleGetProjects = async (res, searchParams) => {
    const filterType = searchParams.get("type");
    const items = await readIndex();
    const filtered = filterType ? items.filter((item) => item.type === filterType) : items;
    sendJSON(res, 200, filtered);
};

const handleGetProject = async (res, projectId) => {
    const safeId = safeProjectId(projectId);
    if (!safeId) {
        sendText(res, 400, "Invalid project id");
        return;
    }
    const projectPath = path.join(DATA_DIR, `${safeId}.json`);
    if (!existsSync(projectPath)) {
        sendText(res, 404, "Project not found");
        return;
    }
    try {
        const raw = await fs.readFile(projectPath, "utf8");
        const parsed = JSON.parse(raw);
        sendJSON(res, 200, parsed);
    } catch (err) {
        console.error("[server] failed to read project file", err);
        sendText(res, 500, "Failed to read project");
    }
};

const handleCreateProject = async (req, res) => {
    let payload;
    try {
        payload = await parseJSONBody(req);
    } catch (err) {
        sendText(res, 400, err.message || "Invalid request body");
        return;
    }
    try {
        const project = buildProjectPayload(payload);
        await processUploads(project);
        const projectPath = path.join(DATA_DIR, `${project.id}.json`);
        await fs.writeFile(projectPath, JSON.stringify(project, null, 2), "utf8");

        const indexItems = await readIndex();
        const summary = {
            id: project.id,
            name: project.name,
            type: project.type,
            ownerId: project.ownerId,
            createdAt: project.createdAt,
            updatedAt: project.updatedAt
        };
        const nextIndex = [summary, ...indexItems.filter((item) => item.id !== project.id)];
        await writeIndex(nextIndex);

        sendJSON(res, 201, project);
    } catch (err) {
        console.error("[server] failed to create project", err);
        sendText(res, 400, err.message || "Failed to create project");
    }
};

const handlePatchProject = async (req, res, projectId) => {
    const safeId = safeProjectId(projectId);
    if (!safeId) {
        sendText(res, 400, "Invalid project id");
        return;
    }
    const projectPath = path.join(DATA_DIR, `${safeId}.json`);
    if (!existsSync(projectPath)) {
        sendText(res, 404, "Project not found");
        return;
    }
    let payload = {};
    try {
        payload = await parseJSONBody(req);
    } catch (err) {
        sendText(res, 400, err.message || "Invalid JSON");
        return;
    }
    if (!payload || typeof payload !== "object") {
        sendText(res, 400, "Invalid payload");
        return;
    }
    try {
        const raw = await fs.readFile(projectPath, "utf8");
        const project = JSON.parse(raw);
        const now = new Date().toISOString();
        if (typeof payload.name === "string") {
            project.name = payload.name.trim();
        }
        if (payload.type === "template" || payload.type === "project") {
            project.type = payload.type;
        }
        project.updatedAt = now;
        await fs.writeFile(projectPath, JSON.stringify(project, null, 2), "utf8");

        const items = await readIndex();
        const nextIndex = items.map((item) => {
            if (item.id !== safeId) return item;
            return {
                ...item,
                name: project.name,
                type: project.type,
                updatedAt: project.updatedAt
            };
        });
        await writeIndex(nextIndex);
        sendJSON(res, 200, project);
    } catch (err) {
        console.error("[server] failed to patch project", err);
        sendText(res, 500, "Failed to update project");
    }
};

const handlePutProject = async (req, res, projectId) => {
    const safeId = safeProjectId(projectId);
    if (!safeId) {
        sendText(res, 400, "Invalid project id");
        return;
    }
    const projectPath = path.join(DATA_DIR, `${safeId}.json`);
    if (!existsSync(projectPath)) {
        sendText(res, 404, "Project not found");
        return;
    }
    let payload = {};
    try {
        payload = await parseJSONBody(req);
    } catch (err) {
        sendText(res, 400, err.message || "Invalid JSON");
        return;
    }
    try {
        const raw = await fs.readFile(projectPath, "utf8");
        const existing = JSON.parse(raw);
        const updated = buildUpdatedProject(payload, { ...existing, id: safeId });
        await fs.writeFile(projectPath, JSON.stringify(updated, null, 2), "utf8");
        const items = await readIndex();
        const nextIndex = items.map((item) => {
            if (item.id !== safeId) return item;
            return {
                ...item,
                name: updated.name,
                type: updated.type,
                updatedAt: updated.updatedAt
            };
        });
        await writeIndex(nextIndex);
        sendJSON(res, 200, updated);
    } catch (err) {
        console.error("[server] failed to save project", err);
        sendText(res, 500, "Failed to save project");
    }
};

const handleDeleteProject = async (res, projectId) => {
    const safeId = safeProjectId(projectId);
    if (!safeId) {
        sendText(res, 400, "Invalid project id");
        return;
    }
    const projectPath = path.join(DATA_DIR, `${safeId}.json`);
    if (!existsSync(projectPath)) {
        sendText(res, 404, "Project not found");
        return;
    }
    try {
        await fs.unlink(projectPath);
    } catch (err) {
        console.error("[server] failed to delete project file", err);
    }
    // remove uploads folder if exists
    const uploadDir = path.join(UPLOADS_DIR, safeId);
    try {
        const stat = await fs.stat(uploadDir);
        if (stat.isDirectory()) {
            await fs.rm(uploadDir, {recursive: true, force: true});
        }
    } catch (_) {
        // ignore
    }
    const items = await readIndex();
    const filtered = items.filter((item) => item.id !== safeId);
    await writeIndex(filtered);
    sendJSON(res, 200, {deleted: true, id: safeId});
};

const requestHandler = async (req, res) => {
    const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);

    if (req.method === "OPTIONS") {
        setCors(res);
        res.statusCode = 204;
        res.end();
        return;
    }

    if (url.pathname.startsWith("/api/projects")) {
        if (req.method === "GET" && url.pathname === "/api/projects") {
            return handleGetProjects(res, url.searchParams);
        }
        if (req.method === "GET" && /^\/api\/projects\/[^/]+/.test(url.pathname)) {
            const projectId = url.pathname.split("/")[3];
            return handleGetProject(res, projectId);
        }
        if (req.method === "POST" && url.pathname === "/api/projects") {
            return handleCreateProject(req, res);
        }
        if (req.method === "PATCH" && /^\/api\/projects\/[^/]+/.test(url.pathname)) {
            const projectId = url.pathname.split("/")[3];
            return handlePatchProject(req, res, projectId);
        }
        if (req.method === "PUT" && /^\/api\/projects\/[^/]+/.test(url.pathname)) {
            const projectId = url.pathname.split("/")[3];
            return handlePutProject(req, res, projectId);
        }
        if (req.method === "DELETE" && /^\/api\/projects\/[^/]+/.test(url.pathname)) {
            const projectId = url.pathname.split("/")[3];
            return handleDeleteProject(res, projectId);
        }
        sendText(res, 405, "Method Not Allowed");
        return;
    }

    if (req.method === "POST" && url.pathname === "/api/uploads") {
        return handleRawUpload(req, res, url);
    }

    if (req.method === "GET" && url.pathname === "/api/conversions/status") {
        return handleConversionStatus(res, url.searchParams);
    }

    await serveStatic(req, res, url);
};

const start = async () => {
    await ensureStorage();

    const startServer = (port, retries = 3) => {
        const server = http.createServer((req, res) => {
            requestHandler(req, res).catch((err) => {
                console.error("[server] unhandled error", err);
                sendText(res, 500, "Internal Server Error");
            });
        });

        server.on("error", (err) => {
            if (err.code === "EADDRINUSE" && retries > 0) {
                const nextPort = port + 1;
                console.warn(`[server] port ${port} is busy, trying ${nextPort}...`);
                startServer(nextPort, retries - 1);
            } else {
                console.error("[server] failed to start", err);
                process.exit(1);
            }
        });

        server.listen(port, () => {
            console.log(`[server] listening on http://localhost:${port}`);
        });
    };

    startServer(PORT);
};

start().catch((err) => {
    console.error("[server] failed to start", err);
    process.exit(1);
});
