#!/usr/bin/env python3
"""Build the Blacksite editor while publishing exact crate progress."""

from __future__ import annotations

import argparse
import json
import os
from pathlib import Path
import subprocess
from typing import Iterable


def write_status(
    path: Path,
    phase: str,
    detail: str,
    progress: int,
    state: str = "running",
    crate_count: str = "",
) -> None:
    temporary = path.with_suffix(path.suffix + ".tmp")
    temporary.write_text(
        f"{phase}\n{detail}\n{progress}\n{state}\n{crate_count}\n",
        encoding="utf-8",
    )
    os.replace(temporary, path)


def unique_crates(tree_output: str) -> set[str]:
    return {line.strip() for line in tree_output.splitlines() if line.strip()}


def completed_crate(
    message: object, completed_package_ids: set[str]
) -> tuple[bool, str]:
    if not isinstance(message, dict) or message.get("reason") != "compiler-artifact":
        return False, ""
    package_id = message.get("package_id")
    if not isinstance(package_id, str) or package_id in completed_package_ids:
        return False, ""
    completed_package_ids.add(package_id)
    target = message.get("target")
    if isinstance(target, dict) and isinstance(target.get("name"), str):
        return True, target["name"]
    return True, package_id.rsplit("#", 1)[-1]


def cargo_tree(cargo: str) -> list[str]:
    return [
        cargo,
        "tree",
        "-p",
        "editor",
        "--features",
        "dev",
        "--edges",
        "normal,build",
        "--prefix",
        "none",
        "--format",
        "{p}",
        "--no-dedupe",
        "--color",
        "never",
    ]


def cargo_build(cargo: str) -> list[str]:
    return [
        cargo,
        "build",
        "-p",
        "editor",
        "--bins",
        "--features",
        "dev",
        "--message-format=json-render-diagnostics",
        "--color",
        "never",
    ]


def rendered_lines(message: object, raw_line: str) -> Iterable[str]:
    if not isinstance(message, dict):
        yield raw_line
        return
    if message.get("reason") == "compiler-message":
        diagnostic = message.get("message")
        if isinstance(diagnostic, dict):
            rendered = diagnostic.get("rendered")
            if isinstance(rendered, str) and rendered:
                yield rendered.rstrip("\n")


def run(status_path: Path, log_path: Path, cargo: str) -> int:
    write_status(
        status_path,
        "Preparing build",
        "Resolving editor crate graph",
        8,
    )
    tree = subprocess.run(
        cargo_tree(cargo),
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=False,
    )
    crates = unique_crates(tree.stdout) if tree.returncode == 0 else set()
    if not crates:
        with log_path.open("a", encoding="utf-8") as log:
            log.write(tree.stdout)
            log.write(tree.stderr)
        write_status(
            status_path,
            "Build graph failed",
            "Open the launch log for Cargo details",
            8,
            "error",
        )
        return tree.returncode or 1

    total = len(crates)
    write_status(
        status_path,
        "Compiling project",
        "Preparing editor crates",
        12,
        crate_count=f"0/{total}",
    )

    process = subprocess.Popen(
        cargo_build(cargo),
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        bufsize=1,
    )
    assert process.stdout is not None
    completed_package_ids: set[str] = set()
    with log_path.open("a", encoding="utf-8") as log:
        for raw_line in process.stdout:
            stripped = raw_line.rstrip("\n")
            try:
                message: object = json.loads(stripped)
            except json.JSONDecodeError:
                message = stripped

            for rendered in rendered_lines(message, stripped):
                log.write(rendered + "\n")
                log.flush()

            changed, crate_name = completed_crate(message, completed_package_ids)
            if not changed:
                continue
            completed = min(len(completed_package_ids), total)
            progress = min(78, 12 + (66 * completed // total))
            write_status(
                status_path,
                "Compiling project",
                f"{crate_name} ready",
                progress,
                crate_count=f"{completed}/{total}",
            )

    return_code = process.wait()
    completed = min(len(completed_package_ids), total)
    if return_code != 0:
        write_status(
            status_path,
            "Build failed",
            "Open the launch log for compiler details",
            max(12, min(78, 12 + (66 * completed // total))),
            "error",
            f"{completed}/{total}",
        )
        return return_code

    write_status(
        status_path,
        "Build complete",
        "Preparing engine process",
        78,
        crate_count=f"{total}/{total}",
    )
    return 0


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("status_path", type=Path)
    parser.add_argument("log_path", type=Path)
    parser.add_argument("--cargo", default="/usr/bin/cargo")
    arguments = parser.parse_args()
    return run(arguments.status_path, arguments.log_path, arguments.cargo)


if __name__ == "__main__":
    raise SystemExit(main())
