"""Finish the install: dependencies, then ffmpeg if this machine has none.

Run once by the installer, and safe to run again — it is also the repair
path when someone's antivirus eats a package. Nothing here is clever; the
value is that it says what it is doing, because a silent five-minute pause
after an installer finishes reads as a hang.
"""
import os
import subprocess
import sys
import urllib.request
from pathlib import Path

HERE = Path(__file__).resolve().parent
ROOT = HERE.parent
PY = ROOT / "python" / ("python.exe" if os.name == "nt" else "bin/python3")
GET_PIP = "https://bootstrap.pypa.io/get-pip.py"


# pip's default cache is %LOCALAPPDATA%\pip\cache -- shared, mutable, and
# not ours. A previous elevated pip run leaves admin-owned files in there, and
# antivirus locks freshly written wheels; either way the build of an sdist-only
# package (pywebview pulls proxy_tools, which has no wheel) dies with
# "[Errno 13] Permission denied" on a path that has nothing to do with us.
# Our own directory cannot be broken by anything the user did last year, and
# it makes the repair path fast because a re-run does not re-download.
CACHE = ROOT / "pip-cache"

LOG = ROOT / "first_run.log"
PROGRESS = ROOT / "first_run.progress"      # <step>|<total>|<label>
DONE = ROOT / "first_run.done"              # exit code, written last

# Named up front so the installer can show the whole list before any of it
# has happened — a progress bar that reveals its steps one at a time cannot
# be distinguished from one that is stuck.
STEPS = [
    "Preparing the bundled interpreter",
    "Installing the package manager",
    "Installing build tools",
    "Installing dependencies",
    "Setting up ffmpeg",
    "Checking the install",
]


def step(n: int, note: str = ""):
    """Tell the installer where we are. Best effort: a setup that cannot
    write its progress file must still finish the setup."""
    try:
        PROGRESS.write_text(
            f"{n}|{len(STEPS)}|{note or STEPS[min(n, len(STEPS)) - 1]}",
            encoding="utf-8")
    except Exception:
        pass
    say(f"[{n}/{len(STEPS)}] {note or STEPS[min(n, len(STEPS)) - 1]}")


def say(msg):
    """Print AND log. The installer does not capture this output, so without
    the file a failure here is invisible — the install reports success and
    the user gets an app whose imports all fail."""
    line = f"  {msg}"
    print(line, flush=True)
    try:
        with open(LOG, "a", encoding="utf-8", errors="replace") as fh:
            fh.write(line + "\n")
    except Exception:
        pass


def run_pip(args: list, what: str) -> int:
    """pip install, with our cache, falling back to no cache at all.

    The fallback matters: if the cache directory is itself the problem --
    disk full, antivirus, a path Windows will not accept -- then not caching
    is slower and works, which beats faster and fails."""
    for attempt, extra in enumerate(([f"--cache-dir={CACHE}"], ["--no-cache-dir"])):
        r = subprocess.run([str(PY), "-m", "pip", "install",
                            "--no-warn-script-location"] + extra + args,
                           text=True, capture_output=True)
        for line in (r.stdout or "").splitlines()[-8:]:
            say("pip: " + line[:200])
        if r.returncode == 0:
            return 0
        # Errors are what someone reads to fix this, so they are not clipped
        # to 200 characters the way progress chatter is. The path in the
        # OSError that sent us here was cut off mid-word, which is exactly
        # the detail that identifies the problem.
        for line in (r.stderr or "").splitlines()[-25:]:
            say("pip! " + line[:600])
        if attempt == 0:
            say(f"{what} failed; retrying without the package cache...")
    return r.returncode


def have_pip() -> bool:
    return subprocess.run([str(PY), "-m", "pip", "--version"],
                          capture_output=True).returncode == 0


def bootstrap_pip():
    """The embeddable distro ships without pip on purpose."""
    say("installing the package manager...")
    tmp = ROOT / "get-pip.py"
    with urllib.request.urlopen(GET_PIP, timeout=120) as r:
        tmp.write_bytes(r.read())
    r = subprocess.run([str(PY), str(tmp), "--no-warn-script-location",
                        f"--cache-dir={CACHE}"],
                       text=True, capture_output=True)
    for line in ((r.stdout or "") + (r.stderr or "")).splitlines()[-6:]:
        say("get-pip: " + line[:200])
    if r.returncode != 0:
        raise RuntimeError(f"get-pip failed ({r.returncode})")
    tmp.unlink(missing_ok=True)


def enable_site():
    """Embedded Python ignores site-packages until its ._pth says otherwise,
    which is the difference between pip working and pip installing into a
    directory nothing ever reads."""
    for pth in (ROOT / "python").glob("python*._pth"):
        text = pth.read_text()
        if "import site" in text and not text.count("#import site"):
            continue
        pth.write_text(text.replace("#import site", "import site")
                       if "#import site" in text else text + "\nimport site\n")
        say(f"enabled site-packages in {pth.name}")


def main():
    print(f"\n  Setting up Clipshelf in {ROOT}\n")
    if not PY.exists():
        sys.exit(f"  the bundled interpreter is missing at {PY}")

    step(1)
    enable_site()
    if not have_pip():
        step(2)
        bootstrap_pip()

    # get-pip installs pip and nothing else. Anything in the tree that
    # ships as a source archive then fails with "Cannot import
    # setuptools.build_meta" — pywebview pulls proxy_tools, which is sdist
    # only, so this is not an edge case.
    step(3)
    code = run_pip(["setuptools", "wheel"], "build tools")
    if code != 0:
        say(f"could not install build tools (exit {code})")
        sys.exit(code)

    step(4, "Installing dependencies (this is the slow one)")
    req = ROOT / "requirements.txt"
    # captured, not streamed: this runs under the installer, which shows no
    # console, so pip's own error was the one thing missing when this failed
    code = run_pip(["-r", str(req)], "dependency install")
    if code != 0:
        say(f"dependency install FAILED (exit {code})")
        say("retry from the Start menu shortcut once the cause is fixed")
        sys.exit(code)

    # ffmpeg: only if the machine has none. It is fetched rather than bundled
    # because the builds carrying libx264 are GPL, and shipping one inside a
    # proprietary installer means taking on obligations for that binary.
    step(5)
    sys.path.insert(0, str(ROOT / "src"))
    try:
        from clipshelf import ffmpeg_setup
        if ffmpeg_setup.available():
            say(f"ffmpeg found at {ffmpeg_setup.find()}")
        else:
            say("downloading ffmpeg (about 80 MB)...")
            res = ffmpeg_setup.install()
            say(f"ffmpeg: {res.get('path') or res.get('error')}")
    except Exception as e:
        say(f"could not set up ffmpeg automatically ({e})")
        say("the app will offer to download it again on first launch")

    # prove it: a dependency that installs but cannot be imported is the
    # failure mode this whole file exists to avoid
    step(6)
    check = subprocess.run(
        [str(PY), "-c", "import fastapi, uvicorn; print('imports ok')"],
        capture_output=True, text=True)
    say((check.stdout or check.stderr).strip()[:200])
    if check.returncode != 0:
        say("dependencies installed but cannot be imported — site-packages "
            "is probably not on the interpreter's path")
        sys.exit(3)

    print("\n  Done. Clipshelf is ready.\n")


if __name__ == "__main__":
    # The installer waits on this file, not on the process: Inno cannot read
    # a [Run] step's exit code once it stops blocking, and it has to stop
    # blocking to draw anything. Written in a finally so a crash is still an
    # answer — a wizard waiting forever is worse than one reporting failure.
    code = 0
    try:
        main()
    except SystemExit as e:
        code = int(e.code or 0)
    except Exception as e:                                      # noqa: BLE001
        say(f"setup failed: {type(e).__name__}: {e}")
        code = 1
    finally:
        try:
            DONE.write_text(str(code), encoding="utf-8")
        except Exception:
            pass
    sys.exit(code)
