import express from "express";
import cors from "cors";
import bcrypt from "bcrypt";
import fileUpload from "express-fileupload";
import path from "path";
import childProcess from "child_process";
import levenshtein from "js-levenshtein";
import cookieParser from "cookie-parser";
import { fileURLToPath } from 'url';

const users = [];
let sessions = [];
const videos = [];

function randomString(length) {
    const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789";
    let result = "";
    for (let i = 0; i < length; ++i) {
        result += chars[Math.floor(chars.length * Math.random())];
    }
    return result;
}

function dirname() {
    return path.dirname(fileURLToPath(import.meta.url));
}

const app = express();
app.use(cors());
app.use(express.json());
app.use(cookieParser());
app.use(express.urlencoded({ extended: true }));

app.use("/", express.static("public/"));
app.use("/videos", express.static("videos/"));

app.post("/api/register", async (req, res) => {
    const { username, password } = req.body;
    if (typeof username !== "string" || typeof password !== "string") {
        return res.status(400).json({ ok: false, error: "bad request" });
    }
    const existingUser = users.find(user => user.username === username);
    if (existingUser !== undefined) {
        return res.status(400).json({ ok: false, error: "username taken" });
    }
    const passwordHash = await bcrypt.hash(password, 10);
    const id = users.length;
    const user = { id, username, passwordHash };
    users.push(user);
    return res.status(200).json({ ok: true, user });
});

app.post("/api/login", async (req, res) => {
    const { username, password } = req.body;
    if (typeof username !== "string" || typeof password !== "string") {
        return res.status(400).json({ ok: false, error: "bad request" });
    }
    const user = users.find(user => user.username === username);
    if (user === undefined) {
        return res.status(400).json({ ok: false, error: "wrong username/password" });
    }
    if (!await bcrypt.compare(password, user.passwordHash)) {
        return res.status(400).json({ ok: false, error: "wrong username/password" });
    }
    sessions = sessions.filter(session => session.userId !== user.id);
    const token = randomString(64);
    const session = { userId: user.id, token };
    sessions.push(session);
    res.clearCookie("token");
    res.cookie("token", token);
    return res.status(200).json({ ok: true, session });
});


app.get("/api/search", async (req, res) => {
    const page = +req.query.page || 0;
    const search = req.query.query;
    if (!search) {
        return res.status(400).json({ ok: false, error: "bad request" });
    }
    const [start, end] = [20 * page, 20 * (page + 1)];
    const withDistance = videos
        .map(video => ({ dist: levenshtein(search, video.title), ...video }));
    withDistance.sort((a, b) => a.dist - b.dist);
    const returnedVideos = withDistance
        .slice(start, end)
        .map(video => {
            const user = users.find(user => user.id === video.userId);
            if (!user) {
                return { ...video, author: "[Liberal]" };
            }
            return { ...video, author: user.username };
        });
    return res.status(200).json({ ok: true, videos: returnedVideos, total: videos.length });
});

function authorized() {
    return (req, res, next) => {
        const token = (() => {
            if (req.cookies && "token" in req.cookies) {
                return req.cookies["token"];
            } else if ("token" in req.query) {
                return req.query["token"];
            } else if (req.method === "post" && "token" in req.body) {
                return req.body["token"];
            } else {
                return null;
            }
        })();
        if (token === null) {
            return res.status(400).json({ ok: false, error: "unauthorized" });
        }
        const session = sessions.find(session => session.token === token);
        if (session === undefined) {
            return res.status(400).json({ ok: false, error: "unauthorized" });
        }
        const user = users.find(user => user.id === session.userId);
        if (user === undefined) {
            throw new Error("error: session with invalid userId");
        }
        req.user = user;
        next();
    }
}

app.get("/api/logout", authorized(), (req, res) => {
    sessions = sessions.filter(session => session.userId !== req.user.id);
    return res.status(200).json({ ok: true });
});

app.post("/api/upload_video", authorized(), fileUpload({ limits: { fileSize: 2 ** 26 }, useTempFiles: true }), async (req, res) => {
    const { title } = req.body;
    if (!req.files || !req.files.video) {
        return res.status(400).json({ ok: false, error: "bad request" });
    }
    if (req.files.video.mimetype !== "video/mp4") {
        return res.status(400).json({ ok: false, error: "bad mimetype" });
    }
    const userId = req.user.id;
    const id = randomString(4);
    const tempPath = req.files.video.tempFilePath;
    const newPath = path.join(dirname(), "videos", `${id}.mp4`);
    console.log(newPath);

    const exitCode = await new Promise((resolve, _reject) => {
        const process = childProcess.spawn("HandBrakeCLI", ["-i", tempPath, "-o", newPath, "-Z", "Social 25 MB 5 Minutes 360p60"]);
        process.stderr.on("data", (data) => {
            console.error(data.toString());
        });
        process.on("close", (code) => {
            resolve(code);
        })
    })
    if (exitCode !== 0) {
        throw new Error("handbrake failed");
    }

    const video = {
        id,
        userId,
        title,
        path: newPath,
    };
    videos.push(video);
    return res.status(200).json({ ok: true, video });
})

app.use((err, req, res, next) => {
    console.error(err);
    res.status(500).json({ ok: false, error: "server error" })
});

app.listen(8000, () => {
    console.log("app at http://localhost:8000/");
})