#!/usr/bin/env python3
"""H5P Repository Mirror Manager.

Two commands:

**init** — discovers H5P repos in a GitHub org, reads ``library.json``
to extract the machineName, creates Bitbucket repos, and writes
``mirror_index.json`` keyed by machineName.

**sync** — reads ``mirror_index.json``, ensures bare clones exist,
fetches from origin, and pushes to the mirror remote.  Pure git — no
Bitbucket API calls.

Usage::

    python -m ...repository mirror init \\
        --mirror-base-url git@bitbucket.org:workspace \\
        --bb-project H5PMIR

    python -m ...repository mirror sync [machine_name]
"""
import argparse
from dataclasses import dataclass
import logging
import os
from pathlib import Path
from typing import Any, Dict, Optional
from typing import ClassVar, List, Type

from byteb4rb1e.utils.argparse import CLICommand, CLI
from byteb4rb1e.utils.vcs import git
from byteb4rb1e.utils.saas import bitbucket

from byteb4rb1e.sphinxcontrib.h5p.utils.pkg.h5porg.repository.index import load_index
from byteb4rb1e.sphinxcontrib.h5p.utils.pkg.h5porg.repository import (
    CACHE_DIR,
    INDEX,
)


MIRROR_REMOTE = "mirror"
MIRROR_BITBUCKET_PROJECT = "H5PMIR"

logger = logging.getLogger(__name__)


def mirror(
    machine_name: Optional[str],
    mirror_index_path: Path = INDEX,
    cache_dir: Path = CACHE_DIR,
    bb_project: str = MIRROR_BITBUCKET_PROJECT,
) -> int:
    """Fetch from origin and push to mirror.  Pure git."""

    bb_token = os.getenv("BITBUCKET_TOKEN")

    if bb_project and not bb_token:
        logger.error(
            "BITBUCKET_TOKEN is required when --bb-project is set."
        )
        return 1

    mirror_index = load_index(mirror_index_path)
    if not mirror_index:
        logger.error(
            "Mirror index is empty or missing (%s). "
            "Run mirror init first.",
            mirror_index_path,
        )
        return 1

    if machine_name:
        if machine_name not in mirror_index:
            logger.error(
                "Machine name '%s' not found in mirror index.",
                machine_name,
            )
            return 1
        entries = {machine_name: mirror_index[machine_name]}
    else:
        entries = mirror_index

    synced = 0

    for mn, entry in entries.items():
        origin = entry["origin"]
        mirror_url = entry["mirror"]

        try:
            bare_path = git.bare_path_for_url(origin, cache_dir)

            if not bare_path.exists():
                bare_path.parent.mkdir(parents=True, exist_ok=True)
                git.mirror_clone(origin, bare_path)

            logger.info(f"[{mn}] fetching remote {origin}")

            refs_before = git.show_ref(bare_path)
            git.fetch(bare_path, "origin")
            refs_after = git.show_ref(bare_path)

            if refs_after == refs_before:
                # Local unchanged — verify the mirror actually
                # has content before skipping.  Covers the case
                # where a previous sync cloned but never pushed.
                if not git.has_remote(bare_path, MIRROR_REMOTE):
                    pass  # need to add remote + push below
                elif git.ls_remote(bare_path, MIRROR_REMOTE):
                    logger.info(f"[{mn}] up to date")
                    continue
                else:
                    logger.info(
                        f"[{mn}] mirror empty, pushing",
                    )

            if bb_project:
                workspace = git.parse_base_url(mirror_url)
                name = git.parse_repo_name(mirror_url)

                if not bitbucket.repository_exists(
                    workspace, name, bb_token,  # type: ignore[arg-type]
                ):
                    logger.info(
                        f"[{mn}] creating Bitbucket repository {workspace}/{name}",
                    )
                    resp_bb = bitbucket.create_repository(
                        workspace,  # type: ignore[arg-type]
                        name,
                        bb_token,  # type: ignore[arg-type]
                        project=bb_project,
                        description=f"Mirror of {origin}",
                    )
                    if resp_bb.status_code not in (200, 201):
                        logger.error(
                            "Failed to create %s/%s: HTTP %s %s",
                            workspace, name,
                            resp_bb.status_code, resp_bb.text,
                        )
                        continue

            if not git.has_remote(bare_path, MIRROR_REMOTE):
                git.add_remote(bare_path, MIRROR_REMOTE, mirror_url)

            git.mirror_push(bare_path, MIRROR_REMOTE)
            synced += 1
            logger.info("Synced %s", mn)
        except Exception:
            logger.exception("Failed to sync %s", mn)

    logger.info(
        "Sync completed. "
        "Synced %d/%d.",
        synced, len(entries),
    )
    return 0


@dataclass
class MirrorCommand(CLICommand):
    name: str = "mirror"
    help: str = "fetch from origin and push to mirror"

    def add_arguments(self, parser: argparse.ArgumentParser) -> None:
        parser.add_argument(
            "MACHINE_NAME", nargs="?", default=None,
            help="Sync a single repository by H5P library machine name",
        )
        parser.add_argument(
            "--index", default=str(INDEX),
            help="Path to repository index",
        )
        parser.add_argument(
            "--cache-dir", default=str(CACHE_DIR),
            help="Directory for bare clone cache",
        )
        parser.add_argument(
            "--bb-project", default=MIRROR_BITBUCKET_PROJECT,
            help="Bitbucket project key, when set, creates "
                 "missing Bitbucket repositories",
        )

    def execute(self, args: Any) -> int:
        return mirror(
            machine_name=args.MACHINE_NAME,
            mirror_index_path=Path(args.index),
            cache_dir=Path(args.cache_dir),
            bb_project=args.bb_project,
        )


if __name__ == "__main__":
    cli = CLI(prog="mirror", description=mirror.__doc__)
    cmd = MirrorCommand()
    cmd.add_arguments(cli.parser)
    cmd.execute(cli.parser.parse_args())
