"""Prepare exact image app source for an offline APP image, never activate it."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import py_compile
import re
import shutil
import subprocess
import time

import connection


def run(args, timeout=60):
    return subprocess.run(args, check=True, capture_output=True, text=True, timeout=timeout).stdout


def checksum(path):
    with path.open("rb") as stream:
        return hashlib.file_digest(stream, "sha256").hexdigest()


def inspect_tree(root):
    rows = {}
    size = 0
    for path in root.rglob("*"):
        if path.is_symlink():
            raise ValueError("Source symlink refused")
        if not path.is_file() or "__pycache__" in path.parts:
            continue
        relative = str(path.relative_to(root))
        if path.suffix not in {".py", ".json", ".txt", ".tmpl"}:
            raise ValueError("Unexpected app file: " + relative)
        size += path.stat().st_size
        if size > 100*1024**2 or len(rows) > 5000:
            raise ValueError("Source size bound exceeded")
        if path.suffix == ".py":
            compile(path.read_bytes(), relative, "exec")
        rows[relative] = checksum(path)
    if not rows or "main.py" not in rows:
        raise ValueError("App source missing")
    return rows


def prepare(name):
    if os.geteuid() != 0 or not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_.-]{0,60}", name):
        raise ValueError("sudo and safe release name required")
    compose_path = connection.active_compose()
    compose = json.loads(compose_path.read_text())
    image = compose["services"]["analytics"]["image"]
    info = json.loads(run(["docker", "image", "inspect", image]))[0]
    destination = Path("/workspace/contextra/source") / name
    if destination.exists():
        raise ValueError("Source directory exists; never overwrite field edits")
    proposal = Path("/var/lib/contextra-maintenance/source") / name
    if proposal.exists():
        raise ValueError("Existing proposal preserved")
    destination.mkdir(mode=0o750, parents=True)
    proposal.mkdir(mode=0o700, parents=True)
    container = None
    try:
        container = run(["docker", "create", "--network", "none", "--name", "contextra-source-extract-"+name,
            "--entrypoint", "/bin/true", info["Id"]]).strip()
        run(["docker", "cp", container + ":/opt/contextra/app", str(destination)], 120)
        for cache in (destination / "app").rglob("__pycache__"):
            if cache.is_symlink():
                raise ValueError("Image cache symlink refused")
            shutil.rmtree(cache)
        files = inspect_tree(destination / "app")
        shutil.copytree(destination / "app", proposal / "baseline-app")
        record = dict(schema=1, status="SOURCE_PREPARED_NOT_ACTIVATED", image=image,
            image_id=info["Id"], source=str(destination), files=files,
            compose=str(compose_path), compose_sha256=checksum(compose_path), unit_sha256=checksum(connection.UNIT),
            prepared_unix=time.time())
        (proposal / "record.json").write_text(json.dumps(record, indent=2))
        plan = dict(method="offline-app-image",base_image=info["Id"],source=str(destination / "app"),host_bind=False)
        (proposal / "app-image-plan.json").write_text(json.dumps(plan, indent=2))
        for path in [destination, *destination.rglob("*")]:
            os.chown(path, 1001, 1001)
            path.chmod(0o755 if path.is_dir() else 0o644)
        print(json.dumps(dict(status=record["status"], source=str(destination), proposal=str(proposal),
                              image_id=info["Id"], files=len(files))))
    finally:
        if container:
            run(["docker", "rm", container])


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("name")
    prepare(parser.parse_args().name)
