#!/usr/bin/env python3
"""a3 arena upload CLI. Python 3.9+, standard library only."""
import argparse
import getpass
import json
import os
from pathlib import Path
import secrets
import sys
import tempfile
import urllib.error
import urllib.parse
import urllib.request

DEFAULT_SERVER = "https://a3-arena.the7emerald.dev"


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        # Never forward passwords or upload credentials to another location.
        return None


def server_url(value):
    parsed = urllib.parse.urlsplit(value)
    if (parsed.scheme not in ("http", "https") or not parsed.hostname or
            parsed.username or parsed.password or parsed.query or parsed.fragment or
            parsed.path not in ("", "/")):
        raise ValueError("server must be an http:// or https:// address without a path")
    return value.rstrip("/")


def api(server, endpoint, data, content_type="application/json", token=None):
    headers = {"Content-Type": content_type, "Accept": "application/json",
               "User-Agent": "a3-arena-cli/1"}
    if token:
        headers["Authorization"] = "Bearer " + token
    req = urllib.request.Request(server + "/api/cli/" + endpoint, data=data, headers=headers)
    try:
        with urllib.request.build_opener(NoRedirect()).open(req, timeout=60) as response:
            return json.load(response)
    except urllib.error.HTTPError as exc:
        try:
            detail = json.loads(exc.read()).get("detail", f"HTTP {exc.code}")
            if not isinstance(detail, str):
                detail = "Invalid request; check the username, file and class name"
        except (ValueError, AttributeError):
            detail = f"HTTP {exc.code}; check the server address or try again later"
        raise ValueError(detail) from None
    except urllib.error.URLError as exc:
        raise ValueError(f"Cannot reach {server}: {exc.reason}") from None


def save_credentials(path, state):
    path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
    fd, tmp = tempfile.mkstemp(prefix=".arena-", dir=path.parent)
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as stream:
            json.dump(state, stream)
        os.replace(tmp, path)  # mkstemp creates owner-only permissions on POSIX.
    finally:
        if os.path.exists(tmp):
            os.unlink(tmp)


def multipart(code, class_name, note):
    boundary = "arena-" + secrets.token_hex(24)
    parts = []
    for name, value in (("class_name", class_name), ("note", note)):
        parts.append((f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"'
                      f'\r\n\r\n{value}\r\n').encode())
    parts.append((f'--{boundary}\r\nContent-Disposition: form-data; name="file"; '
                  'filename="agent.py"\r\nContent-Type: text/x-python\r\n\r\n').encode())
    parts.extend((code, f"\r\n--{boundary}--\r\n".encode()))
    return b"".join(parts), "multipart/form-data; boundary=" + boundary


def main(argv=None):
    parser = argparse.ArgumentParser(description="Log in and upload agents to a3 arena. Replays are on the website.")
    parser.add_argument("--config", type=Path, default=Path.home() / ".config/a3-arena/credentials.json",
                        help="credential file (use separate files for different accounts)")
    sub = parser.add_subparsers(dest="command", required=True)
    login = sub.add_parser("login", help="log in with your website account")
    login.add_argument("--server", default=DEFAULT_SERVER)
    login.add_argument("--username", "-u")
    upload = sub.add_parser("upload", help="create your team or upload its next version")
    upload.add_argument("file", type=Path)
    upload.add_argument("--class", dest="class_name", default="Agent1")
    upload.add_argument("--note", default="")
    sub.add_parser("logout", help="revoke this CLI login and remove its saved credential")
    args = parser.parse_args(argv)
    try:
        if args.command == "login":
            server = server_url(args.server)
            username = args.username or input("Username: ").strip()
            password = getpass.getpass("Password: ")
            state = api(server, "login", json.dumps({"username": username, "password": password}).encode())
            state["server"] = server
            save_credentials(args.config, state)
            print(f"Logged in as {state['username']} at {server}. Login lasts 14 days.")
            return 0
        if not args.config.is_file():
            raise ValueError("Not logged in; run login first")
        state = json.loads(args.config.read_text(encoding="utf-8"))
        server = server_url(state["server"])
        if args.command == "logout":
            try:
                api(server, "logout", b"{}", token=state["token"])
            except ValueError as exc:
                if "CLI login expired or missing" not in str(exc):
                    raise
            args.config.unlink()
            print("Logged out.")
        else:
            maximum = state.get("max_upload_bytes", 256 * 1024)
            with args.file.open("rb") as stream:
                code = stream.read(maximum + 1)
            if len(code) > maximum:
                raise ValueError(f"File exceeds the server limit of {maximum // 1024} KB")
            code.decode("utf-8")
            data, content_type = multipart(code, args.class_name, args.note)
            result = api(server, "upload", data, content_type, state["token"])
            print(f"Uploaded {result['team']} v{result['version']}; matches queued.")
            print("Results and replays: " + server + result["path"])
        return 0
    except (OSError, ValueError, KeyError) as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1
    except (KeyboardInterrupt, EOFError):
        print("\nCancelled.", file=sys.stderr)
        return 1


if __name__ == "__main__":
    sys.exit(main())
