"""``muse tag`` — protocol-native semantic version tags. Version tags are lightweight, local-first named pointers from a semantic version string (e.g. ``v0.2.0-rc14``) to a commit. They live under ``.muse/version-tags/`` and are independent of the label-annotation system (``muse label``) and the MuseHub-aware release system (``muse release``). Usage:: muse tag add [] [--message ] [--author ] — create or overwrite a version tag muse tag list [--pre] — list version tags (latest first) muse tag read — inspect a single version tag muse tag delete — delete a version tag muse tag latest [--pre] — print the latest version tag Version tag format:: v..[-
][+]

    Examples:
        v0.2.0-rc14          release candidate
        v1.0.0               stable release
        v1.1.0-beta.3        beta with a minor-version bump
        v2.0.0-alpha         first alpha of a major

    Pre-release precedence (descending):
        stable > rc > beta[.] > alpha[.] > generic

JSON output (``--json``) schema for ``tag add``::

    {
      "status":      "tagged | already_tagged",
      "tag_id":      "",
      "tag":         "v0.2.0-rc14",
      "semver":      {"major": 0, "minor": 2, "patch": 0, "pre": "rc14", "build": ""},
      "commit_id":   "",
      "created_at":  "",
      "author":      "gabriel",
      "message":     "Release candidate 14",
      "duration_ms": 1.234,
      "exit_code":   0
    }

JSON output schema for ``tag list``::

    {
      "total": ,
      "tags": [
        {
          "tag_id":     "",
          "tag":        "v0.2.0-rc14",
          "semver":     { ... },
          "commit_id":  "",
          "created_at": "",
          "author":     "gabriel",
          "message":    ""
        }, ...
      ],
      "duration_ms": 1.234,
      "exit_code":   0
    }

Exit codes::

    0 — success
    1 — invalid tag name, commit not found, tag not found
    2 — not inside a Muse repository
"""

import argparse
import datetime
import json
import logging
import sys
from typing import TypedDict

from muse.core.errors import ExitCode
from muse.core.repo import read_repo_id, require_repo
from muse.core.refs import read_current_branch
from muse.core.commits import resolve_commit_ref
from muse.core.version_tags import (
    VersionTagRecord,
    _validate_version_tag_name,
    compute_version_tag_id,
    delete_version_tag,
    list_version_tags,
    read_version_tag,
    semver_key,
    write_version_tag,
)
from muse.core.semver import SemVerTag, parse_semver
from muse.cli.config import get_config_value, get_remote, get_signing_identity
from muse.core.envelope import EnvelopeJson, make_envelope
from muse.core.transport import make_transport, TransportError
from muse.core.validation import sanitize_display
from muse.core.timing import start_timer
from muse.core.types import JsonValue

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Wire-format TypedDicts for CLI output
# ---------------------------------------------------------------------------

class _SemVerJson(TypedDict):
    major: int
    minor: int
    patch: int
    pre: str
    build: str


class _TagEntryJson(TypedDict):
    tag_id: str
    tag: str
    semver: _SemVerJson
    commit_id: str
    created_at: str
    author: str
    message: str


class _TagAddJson(EnvelopeJson):
    status: str
    tag_id: str
    tag: str
    semver: _SemVerJson
    commit_id: str
    created_at: str
    author: str
    message: str


class _TagListJson(EnvelopeJson):
    total: int
    tags: list[_TagEntryJson]


class _TagReadJson(EnvelopeJson):
    tag_id: str
    tag: str
    semver: _SemVerJson
    commit_id: str
    created_at: str
    author: str
    message: str


class _TagDeleteJson(EnvelopeJson):
    status: str
    tag: str
    deleted: bool


class _TagLatestJson(EnvelopeJson):
    latest: str | None
    tag_id: str | None
    semver: _SemVerJson | None
    commit_id: str | None
    created_at: str | None
    author: str | None
    message: str | None


class _TagPushJson(EnvelopeJson):
    status: str     # "pushed" | "already_current" | "conflict"
    tag: str
    remote: str
    commit_id: str


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _semver_to_json(sv: SemVerTag) -> _SemVerJson:
    return _SemVerJson(
        major=sv["major"],
        minor=sv["minor"],
        patch=sv["patch"],
        pre=sv["pre"],
        build=sv["build"],
    )


def _record_to_entry(r: VersionTagRecord) -> _TagEntryJson:
    return _TagEntryJson(
        tag_id=r.tag_id,
        tag=r.tag,
        semver=_semver_to_json(r.semver),
        commit_id=r.commit_id,
        created_at=r.created_at.isoformat(),
        author=r.author,
        message=r.message,
    )


def _default_author(root) -> str:
    """Read user.handle from repo config, fall back to 'unknown'."""
    try:
        val = get_config_value("user.handle", root)
        return val if val else "unknown"
    except Exception:
        return "unknown"


def _is_pre_release(r: VersionTagRecord) -> bool:
    return bool(r.semver["pre"])


# ---------------------------------------------------------------------------
# register
# ---------------------------------------------------------------------------

def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
    """Register the ``muse tag`` subcommand tree."""
    parser = subparsers.add_parser(
        "tag",
        help="Create and query semantic version tags.",
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    subs = parser.add_subparsers(dest="subcommand", metavar="SUBCOMMAND")
    subs.required = True

    # -- add ------------------------------------------------------------------
    add_p = subs.add_parser(
        "add",
        help="Create (or overwrite) a version tag pointing to a commit.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    add_p.add_argument("tag_name", help="Version tag (e.g. v0.2.0-rc14).")
    add_p.add_argument(
        "ref", nargs="?", default=None,
        help="Commit ID or branch name (default: HEAD).",
    )
    add_p.add_argument(
        "--message", "-m", default="",
        help="Optional annotation message for this tag.",
    )
    add_p.add_argument(
        "--author", default=None,
        help="Author name (default: user.handle from repo config).",
    )
    add_p.add_argument("--json", "-j", action="store_true", dest="json_out",
                       help="Emit machine-readable JSON on stdout.")
    add_p.set_defaults(func=run_add, json_out=False)

    # -- list -----------------------------------------------------------------
    list_p = subs.add_parser(
        "list",
        help="List all version tags sorted by SemVer precedence (latest first).",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    list_p.add_argument(
        "--pre", action="store_true",
        help="Include pre-release tags (rc, beta, alpha). By default only stable tags are shown.",
    )
    list_p.add_argument("--json", "-j", action="store_true", dest="json_out",
                        help="Emit machine-readable JSON on stdout.")
    list_p.set_defaults(func=run_list, json_out=False, pre=False)

    # -- read -----------------------------------------------------------------
    read_p = subs.add_parser(
        "read",
        help="Inspect a single version tag.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    read_p.add_argument("tag_name", help="Version tag to inspect (e.g. v0.2.0-rc14).")
    read_p.add_argument("--json", "-j", action="store_true", dest="json_out",
                        help="Emit machine-readable JSON on stdout.")
    read_p.set_defaults(func=run_read, json_out=False)

    # -- delete ---------------------------------------------------------------
    delete_p = subs.add_parser(
        "delete",
        help="Delete a version tag.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    delete_p.add_argument("tag_name", help="Version tag to delete (e.g. v0.2.0-rc14).")
    delete_p.add_argument("--json", "-j", action="store_true", dest="json_out",
                          help="Emit machine-readable JSON on stdout.")
    delete_p.set_defaults(func=run_delete, json_out=False)

    # -- latest ---------------------------------------------------------------
    latest_p = subs.add_parser(
        "latest",
        help="Print the latest version tag (highest SemVer precedence).",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    latest_p.add_argument(
        "--pre", action="store_true",
        help="Consider pre-release tags too. By default only stable tags are considered.",
    )
    latest_p.add_argument("--json", "-j", action="store_true", dest="json_out",
                          help="Emit machine-readable JSON on stdout.")
    latest_p.set_defaults(func=run_latest, json_out=False, pre=False)

    # -- push -----------------------------------------------------------------
    push_p = subs.add_parser(
        "push",
        help="Push a version tag to a remote.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    push_p.add_argument("remote", help="Remote name (e.g. local, origin).")
    push_p.add_argument("tag_name", help="Version tag to push (e.g. v1.0.0).")
    push_p.add_argument(
        "--force", "-f", action="store_true",
        help="Overwrite the tag on the remote if it already exists with a different commit.",
    )
    push_p.add_argument("--json", "-j", action="store_true", dest="json_out",
                        help="Emit machine-readable JSON on stdout.")
    push_p.set_defaults(func=run_push, json_out=False, force=False)


# ---------------------------------------------------------------------------
# run_add
# ---------------------------------------------------------------------------

def run_add(args: argparse.Namespace) -> None:
    """Create (or overwrite) a version tag pointing to a commit."""
    elapsed = start_timer()

    tag_name: str = args.tag_name
    ref: str | None = args.ref
    message: str = args.message
    author_arg: str | None = args.author
    json_out: bool = args.json_out

    def _emit_error(msg: str, code: int, error_key: str = "error", **extra: JsonValue) -> None:
        if json_out:
            payload = {
                **make_envelope(elapsed, exit_code=code),
                "error": error_key,
                "message": msg,
            }
            payload.update(extra)
            print(json.dumps(payload))
        else:
            print(f"❌ {msg}", file=sys.stderr)
        raise SystemExit(code)

    try:
        _validate_version_tag_name(tag_name)
    except ValueError as exc:
        _emit_error(
            f"Invalid version tag: {sanitize_display(str(exc))}",
            ExitCode.USER_ERROR,
            "invalid_tag_name",
        )

    root = require_repo()
    repo_id = read_repo_id(root)
    branch = read_current_branch(root)
    author = author_arg if author_arg else _default_author(root)

    commit = resolve_commit_ref(root, branch, ref)
    if commit is None:
        _emit_error(
            f"Commit '{sanitize_display(str(ref))}' not found.",
            ExitCode.USER_ERROR,
            "commit_not_found",
            ref=str(ref),
        )

    semver = parse_semver(tag_name)
    tag_id = compute_version_tag_id(repo_id=repo_id, tag=tag_name)
    created_at = datetime.datetime.now(datetime.timezone.utc)

    existing = read_version_tag(root, tag_name)
    status = "already_tagged" if existing else "tagged"

    record = VersionTagRecord(
        tag_id=tag_id,
        repo_id=repo_id,
        tag=tag_name,
        semver=semver,
        commit_id=commit.commit_id,
        created_at=created_at if not existing else existing.created_at,
        author=author if not existing else existing.author,
        message=message if message else (existing.message if existing else ""),
    )
    write_version_tag(root, record)

    if json_out:
        print(json.dumps(_TagAddJson(
            **make_envelope(elapsed),
            status=status,
            tag_id=record.tag_id,
            tag=record.tag,
            semver=_semver_to_json(record.semver),
            commit_id=record.commit_id,
            created_at=record.created_at.isoformat(),
            author=record.author,
            message=record.message,
        )))
    else:
        action = "Tagged" if status == "tagged" else "Tag already exists —"
        print(f"{action} {record.commit_id} as {sanitize_display(record.tag)}")


# ---------------------------------------------------------------------------
# run_list
# ---------------------------------------------------------------------------

def run_list(args: argparse.Namespace) -> None:
    """List all version tags sorted by SemVer precedence (latest first)."""
    elapsed = start_timer()

    include_pre: bool = args.pre
    json_out: bool = args.json_out

    root = require_repo()
    all_tags = list_version_tags(root)

    if not include_pre:
        all_tags = [r for r in all_tags if not _is_pre_release(r)]

    if json_out:
        print(json.dumps(_TagListJson(
            **make_envelope(elapsed),
            total=len(all_tags),
            tags=[_record_to_entry(r) for r in all_tags],
        )))
        return

    if not all_tags:
        print("No version tags.")
        return
    for r in all_tags:
        pre_suffix = f"-{r.semver['pre']}" if r.semver["pre"] else ""
        print(f"{r.tag:<24}  {r.commit_id}  {r.created_at.isoformat()}")


# ---------------------------------------------------------------------------
# run_read
# ---------------------------------------------------------------------------

def run_read(args: argparse.Namespace) -> None:
    """Inspect a single version tag."""
    elapsed = start_timer()

    tag_name: str = args.tag_name
    json_out: bool = args.json_out

    def _emit_error(msg: str, code: int, error_key: str = "error", **extra: JsonValue) -> None:
        if json_out:
            payload = {
                **make_envelope(elapsed, exit_code=code),
                "error": error_key,
                "message": msg,
            }
            payload.update(extra)
            print(json.dumps(payload))
        else:
            print(f"❌ {msg}", file=sys.stderr)
        raise SystemExit(code)

    try:
        _validate_version_tag_name(tag_name)
    except ValueError as exc:
        _emit_error(
            f"Invalid version tag: {sanitize_display(str(exc))}",
            ExitCode.USER_ERROR,
            "invalid_tag_name",
        )

    root = require_repo()
    record = read_version_tag(root, tag_name)
    if record is None:
        _emit_error(
            f"Version tag '{sanitize_display(tag_name)}' not found.",
            ExitCode.USER_ERROR,
            "tag_not_found",
            tag=tag_name,
        )

    if json_out:
        print(json.dumps(_TagReadJson(
            **make_envelope(elapsed),
            tag_id=record.tag_id,
            tag=record.tag,
            semver=_semver_to_json(record.semver),
            commit_id=record.commit_id,
            created_at=record.created_at.isoformat(),
            author=record.author,
            message=record.message,
        )))
    else:
        print(f"tag:       {sanitize_display(record.tag)}")
        print(f"commit_id: {record.commit_id}")
        print(f"author:    {sanitize_display(record.author)}")
        print(f"created:   {record.created_at.isoformat()}")
        if record.message:
            print(f"message:   {sanitize_display(record.message)}")


# ---------------------------------------------------------------------------
# run_delete
# ---------------------------------------------------------------------------

def run_delete(args: argparse.Namespace) -> None:
    """Delete a version tag."""
    elapsed = start_timer()

    tag_name: str = args.tag_name
    json_out: bool = args.json_out

    def _emit_error(msg: str, code: int, error_key: str = "error", **extra: JsonValue) -> None:
        if json_out:
            payload = {
                **make_envelope(elapsed, exit_code=code),
                "error": error_key,
                "message": msg,
            }
            payload.update(extra)
            print(json.dumps(payload))
        else:
            print(f"❌ {msg}", file=sys.stderr)
        raise SystemExit(code)

    try:
        _validate_version_tag_name(tag_name)
    except ValueError as exc:
        _emit_error(
            f"Invalid version tag: {sanitize_display(str(exc))}",
            ExitCode.USER_ERROR,
            "invalid_tag_name",
        )

    root = require_repo()
    deleted = delete_version_tag(root, tag_name)

    if json_out:
        print(json.dumps(_TagDeleteJson(
            **make_envelope(elapsed),
            status="deleted" if deleted else "not_found",
            tag=tag_name,
            deleted=deleted,
        )))
    else:
        if deleted:
            print(f"Deleted version tag '{sanitize_display(tag_name)}'.")
        else:
            print(f"Version tag '{sanitize_display(tag_name)}' not found — nothing deleted.")


# ---------------------------------------------------------------------------
# run_latest
# ---------------------------------------------------------------------------

def run_latest(args: argparse.Namespace) -> None:
    """Print the latest version tag by SemVer precedence."""
    elapsed = start_timer()

    include_pre: bool = args.pre
    json_out: bool = args.json_out

    root = require_repo()
    all_tags = list_version_tags(root)

    if not include_pre:
        all_tags = [r for r in all_tags if not _is_pre_release(r)]

    if not all_tags:
        if json_out:
            print(json.dumps(_TagLatestJson(
                **make_envelope(elapsed),
                latest=None,
                tag_id=None,
                semver=None,
                commit_id=None,
                created_at=None,
                author=None,
                message=None,
            )))
        else:
            print("No version tags.")
        return

    r = all_tags[0]  # list_version_tags already sorts latest-first
    if json_out:
        print(json.dumps(_TagLatestJson(
            **make_envelope(elapsed),
            latest=r.tag,
            tag_id=r.tag_id,
            semver=_semver_to_json(r.semver),
            commit_id=r.commit_id,
            created_at=r.created_at.isoformat(),
            author=r.author,
            message=r.message,
        )))
    else:
        print(sanitize_display(r.tag))


# ---------------------------------------------------------------------------
# run_push
# ---------------------------------------------------------------------------

def run_push(args: argparse.Namespace) -> None:
    """Push a single version tag to a remote."""
    elapsed = start_timer()

    remote: str = args.remote
    tag_name: str = args.tag_name
    force: bool = args.force
    json_out: bool = args.json_out

    def _emit_error(msg: str, code: int, error_key: str = "error", **extra: JsonValue) -> None:
        if json_out:
            payload = {
                **make_envelope(elapsed, exit_code=code),
                "error": error_key,
                "message": msg,
            }
            payload.update(extra)
            print(json.dumps(payload))
        else:
            print(f"❌ {msg}", file=sys.stderr)
        raise SystemExit(code)

    try:
        _validate_version_tag_name(tag_name)
    except ValueError as exc:
        _emit_error(
            f"Invalid version tag: {sanitize_display(str(exc))}",
            ExitCode.USER_ERROR,
            "invalid_tag_name",
        )

    root = require_repo()

    url = get_remote(remote, root)
    if url is None:
        _emit_error(
            f"Remote '{sanitize_display(remote)}' is not configured. "
            "Add one with: muse remote add  ",
            ExitCode.USER_ERROR,
            "remote_not_configured",
            remote=remote,
        )

    record = read_version_tag(root, tag_name)
    if record is None:
        _emit_error(
            f"Version tag '{sanitize_display(tag_name)}' not found locally. "
            "Create it first with: muse tag add ",
            ExitCode.USER_ERROR,
            "tag_not_found",
            tag=tag_name,
        )

    signing = get_signing_identity(root, remote_url=url)
    transport = make_transport(url)

    tag_dict = {
        "tag_id": record.tag_id,
        "repo_id": record.repo_id,
        "tag": record.tag,
        "semver": {
            "major": record.semver["major"],
            "minor": record.semver["minor"],
            "patch": record.semver["patch"],
            "pre": record.semver["pre"],
            "build": record.semver["build"],
        },
        "commit_id": record.commit_id,
        "created_at": record.created_at.isoformat(),
        "author": record.author,
        "message": record.message,
    }

    try:
        result = transport.push_version_tags(url, signing, [tag_dict], force=force)
    except TransportError as exc:
        if exc.status_code == 409:
            _emit_error(
                f"Tag '{sanitize_display(tag_name)}' conflicts with the remote — "
                "use --force to overwrite.",
                ExitCode.USER_ERROR,
                "conflict",
                tag=tag_name,
                status="conflict",
            )
        else:
            _emit_error(
                f"Push failed: {sanitize_display(str(exc))}",
                ExitCode.USER_ERROR,
                "push_failed",
            )

    stored = result.get("stored", 0)
    status = "pushed" if stored > 0 else "already_current"

    if json_out:
        print(json.dumps(_TagPushJson(
            **make_envelope(elapsed),
            status=status,
            tag=tag_name,
            remote=remote,
            commit_id=record.commit_id,
        )))
    else:
        if status == "pushed":
            print(f"✅ Pushed {sanitize_display(tag_name)} → {sanitize_display(remote)}")
        else:
            print(f"Tag {sanitize_display(tag_name)} already up-to-date on {sanitize_display(remote)}")