import asyncio
import datetime
import json
import logging
import subprocess
from pathlib import Path
from typing import Any, Literal, TypeAlias

from fastapi import FastAPI, Form, HTTPException
from fastapi.responses import FileResponse
from sse_starlette import EventSourceResponse

DataMode: TypeAlias = Literal["holt59", "tests"]

LOGGER = logging.getLogger("uvicorn.info")

GIT_AOC_PATH = Path("/code")
AOC_FILES_PATH = Path("/files")


def update_repository():
    subprocess.run(["git", "fetch"], cwd=GIT_AOC_PATH)
    subprocess.run(["git", "reset", "--hard", "origin/master"], cwd=GIT_AOC_PATH)


def read_head_commit():
    # read git commit
    process = subprocess.run(
        ["git", "log", "-1", '--format="%H"'], cwd=GIT_AOC_PATH, stdout=subprocess.PIPE
    )
    return process.stdout.decode("utf-8").strip()[1:-1]


def read_test_file(year: int, day: int, mode: DataMode) -> str:
    path = GIT_AOC_PATH.joinpath(f"src/holt59/aoc/inputs/{mode}/{year}/day{day}.txt")
    if not path.exists():
        return ""

    with open(path, "r") as fp:
        return fp.read()


app = FastAPI()


@app.post("/update")
def update():
    update_repository()
    head_commit = read_head_commit()
    LOGGER.info(f"repository now at {head_commit}")
    return {"HEAD": head_commit}


@app.get("/data")
async def data(year: int, day: int, mode: DataMode = "tests"):
    return {"content": read_test_file(year, day, mode)}


@app.get("/file")
async def file(name: str):
    path = AOC_FILES_PATH.joinpath(name).resolve()

    if path.parent != AOC_FILES_PATH:
        raise HTTPException(403)

    return FileResponse(path)


@app.post("/submit-sse")
async def submit_sse(year: int = Form(), day: int = Form(), input: str = Form()):
    data = input.rstrip().replace("\r\n", "\n")

    process = await asyncio.create_subprocess_exec(
        "holt59-aoc",
        "--stdin",
        "--api",
        "--output",
        AOC_FILES_PATH,
        "--year",
        str(year),
        str(day),
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        stdin=asyncio.subprocess.PIPE,
    )

    assert process.stdin is not None
    assert process.stdout is not None
    assert process.stderr is not None

    message_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()

    async def watch_stream(
        fp: asyncio.StreamReader, stream: Literal["stdout", "stderr"]
    ):
        while True:
            output = await fp.readline()
            if output == b"":
                break
            await message_queue.put(
                {
                    "type": "stream",
                    "stream": stream,
                    "data": json.loads(output)
                    if stream == "stdout"
                    else {"type": "error", "message": output.decode("utf-8")},
                }
            )

    async def run():
        assert process.stdin is not None
        assert process.stdout is not None
        assert process.stderr is not None

        await message_queue.put(
            {
                "type": "start",
                "data": {
                    "time": datetime.datetime.now().isoformat(),
                    "commit": read_head_commit(),
                },
            }
        )

        process.stdin.write(data.encode("utf-8"))
        await process.stdin.drain()
        process.stdin.close()

        await asyncio.gather(
            # process.stdin.drain(),
            watch_stream(process.stdout, "stdout"),
            watch_stream(process.stderr, "stderr"),
        )

        returncode: int | None = None
        try:
            returncode = await asyncio.wait_for(process.wait(), timeout=300)
        except asyncio.TimeoutError:
            ...

        await message_queue.put(
            {
                "type": "complete",
                "data": {
                    "status": "success" if returncode is not None else "error",
                    "returncode": -1 if returncode is None else -1,
                },
            }
        )

        await message_queue.join()

    loop = asyncio.get_event_loop()
    loop.create_task(run())

    async def stream_queue():
        while True:
            message = await message_queue.get()

            yield json.dumps(
                {
                    "event": message["type"],
                    "id": 0,
                    "retry": 0,
                    "data": message["data"],
                }
            )

            message_queue.task_done()

            if message["type"] == "complete":
                break

    return EventSourceResponse(stream_queue())


@app.post("/submit")
async def submit(
    year: int = Form(), day: int = Form(), verbose: bool = Form(), input: str = Form()
) -> dict[str, Any]:
    data = input.strip().replace("\r\n", "\n")

    args: list[str] = ["holt59-aoc", "--stdin"]
    if verbose:
        args += ["-v"]

    start = datetime.datetime.now()
    process = subprocess.run(
        args + ["--year", str(year), str(day)],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        input=data.encode("utf-8"),
        timeout=300,
    )
    time = datetime.datetime.now() - start

    return {
        "commit": read_head_commit(),
        "input": data,
        "error": process.returncode != 0,
        "stdout": process.stdout.decode("utf-8"),
        "stderr": process.stderr.decode("utf-8"),
        "time": time.total_seconds(),
    }


@app.get("/")
async def root():
    return {"message": "Hello World"}