import * as fs from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
import send from "koa-send";
import rename from "rename";
import { serverLogger } from "../index.js";
import { contentDisposition } from "../../misc/content-disposition.js";
import { DriveFiles } from "../../models/index.js";
import { InternalStorage } from "../../services/drive/internal-storage.js";
import { createTemp } from "../../misc/create-temp.js";
import { downloadUrl } from "../../misc/download-url.js";
import { detectType } from "../../misc/get-file-info.js";
import { convertToWebp } from "../../services/drive/image-processor.js";
import { GenerateVideoThumbnail } from "../../services/drive/generate-video-thumbnail.js";
import { StatusError } from "../../misc/fetch.js";
import { FILE_TYPE_BROWSERSAFE, MINUTE } from "../../const.js";
import { getIpHash } from "../../misc/get-ip-hash.js";
import { limiter } from "../api/limiter.js";
import authenticate from "../api/authenticate.js";
import { isIgnorableConnectionError } from "../is-ignorable-connection-error.js";
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const assets = `${_dirname}/../../server/file/assets/`;
const commonReadableHandlerGenerator = (ctx)=>(e)=>{
        if (isIgnorableConnectionError(e)) return;
        serverLogger.error(e);
        ctx.status = 500;
        ctx.set("Cache-Control", "max-age=300");
    };
export default async function(ctx) {
    const key = ctx.params.key;
    // Fetch drive file
    const file = await DriveFiles.createQueryBuilder("file").where("file.accessKey = :accessKey", {
        accessKey: key
    }).orWhere("file.thumbnailAccessKey = :thumbnailAccessKey", {
        thumbnailAccessKey: key
    }).orWhere("file.webpublicAccessKey = :webpublicAccessKey", {
        webpublicAccessKey: key
    }).getOne();
    if (file == null) {
        ctx.status = 404;
        ctx.set("Cache-Control", "max-age=86400");
        await send(ctx, "/dummy.png", {
            root: assets
        });
        return;
    }
    ctx.set("X-Content-Type-Options", "nosniff");
    const isThumbnail = file.thumbnailAccessKey === key;
    const isWebpublic = file.webpublicAccessKey === key;
    const requestedDownload = ctx.query.download === "1";
    const requestedStream = ctx.query.stream === "1";
    const isStreamableMedia = file.type.startsWith("video/") || file.type.startsWith("audio/");
    // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
    const limitActor = getIpHash(ctx.ip);
    const isMediaPlayback = isStreamableMedia && !requestedDownload;
    const limit = {
        key: `drive-file:${key}`,
        duration: MINUTE * 10,
        max: isMediaPlayback ? 600 : 10
    };
    await limiter(limit, limitActor).catch((e)=>{
        const remainingTime = e.remainingTime ? `Please try again in ${e.remainingTime}.` : "Please try again later.";
        ctx.status = 429;
        ctx.body = "Rate limit exceeded. " + remainingTime;
        ctx.set("Content-Type", "text/plain; charset=utf-8");
        ctx.set("Cache-Control", "no-store");
    });
    if (ctx.status == 429) return;
    const requesterId = requestedDownload ? await getRequesterId(ctx) : null;
    const isOwner = requesterId != null && requesterId === file.userId;
    const protectedOriginal = !isThumbnail && !isWebpublic && !file.allowDownload && !isOwner;
    if (requestedDownload && protectedOriginal) {
        ctx.status = 403;
        ctx.body = "Download is not allowed for this file.";
        ctx.set("Cache-Control", "max-age=300");
        return;
    }
    if (!file.storedInternal) {
        if (file.isLink && file.uri) {
            // 期限切れリモートファイル
            const [path, cleanup] = await createTemp();
            try {
                await downloadUrl(file.uri, path);
                const { mime, ext } = await detectType(path);
                const convertFile = async ()=>{
                    if (isThumbnail) {
                        if ([
                            "image/jpeg",
                            "image/webp",
                            "image/png",
                            "image/svg+xml",
                            "image/avif"
                        ].includes(mime)) {
                            return await convertToWebp(path, 996, 560);
                        } else if (mime.startsWith("video/")) {
                            return await GenerateVideoThumbnail(path);
                        }
                    }
                    if (isWebpublic) {
                        if ([
                            "image/svg+xml"
                        ].includes(mime)) {
                            return await convertToWebp(path, 2048, 2048, 100);
                        }
                    }
                    return {
                        data: fs.readFileSync(path),
                        ext,
                        type: mime
                    };
                };
                const image = await convertFile();
                ctx.body = image.data;
                ctx.set("Content-Type", FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : "application/octet-stream");
                ctx.set("Cache-Control", "max-age=31536000, immutable");
            } catch (e) {
                if (isIgnorableConnectionError(e)) return;
                serverLogger.error(`${e}`);
                if (e instanceof StatusError && !e.isRetryable) {
                    ctx.status = e.statusCode;
                    ctx.set("Cache-Control", "max-age=86400");
                } else {
                    ctx.status = 500;
                    ctx.set("Cache-Control", "max-age=300");
                }
            } finally{
                cleanup();
            }
            return;
        }
        ctx.status = 204;
        ctx.set("Cache-Control", "max-age=86400");
        return;
    }
    if (isThumbnail || isWebpublic) {
        const { mime, ext } = await detectType(InternalStorage.resolvePath(key));
        const filename = rename(file.name, {
            suffix: isThumbnail ? "-thumb" : "-web",
            extname: ext ? `.${ext}` : undefined
        }).toString();
        ctx.body = InternalStorage.read(key);
        ctx.set("Content-Type", FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : "application/octet-stream");
        ctx.set("Cache-Control", "max-age=31536000, immutable");
        ctx.set("Content-Disposition", contentDisposition("inline", filename));
    } else {
        const storageKey = file.accessKey;
        const disposition = requestedDownload ? "attachment" : "inline";
        ctx.set("Content-Type", isStreamableMedia || FILE_TYPE_BROWSERSAFE.includes(file.type) ? file.type : "application/octet-stream");
        ctx.set("Cache-Control", "max-age=31536000, immutable");
        ctx.set("Content-Disposition", contentDisposition(disposition, file.name));
        if (!requestedDownload && isStreamableMedia) {
            const servedRange = await sendRange(ctx, storageKey);
            if (servedRange) return;
        }
        const readable = InternalStorage.read(storageKey);
        readable.on("error", commonReadableHandlerGenerator(ctx));
        ctx.body = readable;
    }
}
async function getRequesterId(ctx) {
    const queryToken = typeof ctx.query.i === "string" ? ctx.query.i : null;
    try {
        const [user] = await authenticate(ctx.get("authorization") || null, queryToken, true);
        return user?.id ?? null;
    } catch  {
        return null;
    }
}
async function sendRange(ctx, key) {
    const range = ctx.get("range");
    if (!range) {
        ctx.set("Accept-Ranges", "bytes");
        return false;
    }
    const path = InternalStorage.resolvePath(key);
    const stat = await fs.promises.stat(path);
    const match = /^bytes=(\d*)-(\d*)$/.exec(range);
    if (!match) {
        ctx.status = 416;
        ctx.set("Content-Range", `bytes */${stat.size}`);
        return true;
    }
    let start = match[1] === "" ? 0 : Number(match[1]);
    let end = match[2] === "" ? stat.size - 1 : Number(match[2]);
    if (match[1] === "" && match[2] !== "") {
        const suffixLength = Number(match[2]);
        start = Math.max(stat.size - suffixLength, 0);
        end = stat.size - 1;
    }
    if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || start < 0 || end >= stat.size) {
        ctx.status = 416;
        ctx.set("Content-Range", `bytes */${stat.size}`);
        return true;
    }
    ctx.status = 206;
    ctx.set("Accept-Ranges", "bytes");
    ctx.set("Content-Range", `bytes ${start}-${end}/${stat.size}`);
    ctx.set("Content-Length", String(end - start + 1));
    const readable = fs.createReadStream(path, {
        start,
        end
    });
    readable.on("error", commonReadableHandlerGenerator(ctx));
    ctx.body = readable;
    return true;
}
