#!/usr/bin/env python3
"""本機 Git remote 只讀盤點工具。

此工具掃描指定 root 底下可見的 Git working tree，讀取 `.git/config`
中的 remote URL，並在輸出前移除 URL 內的帳密。它不會 fetch、push、
修改 remote，也不會連線到 GitHub 或 Gitea。
"""

from __future__ import annotations

import argparse
import configparser
import json
import os
import sys
from pathlib import Path
from urllib.parse import urlsplit, urlunsplit


DEFAULT_EXCLUDE_NAMES = {
    ".cache",
    ".cargo",
    ".claude",
    ".codex",
    ".gemini",
    ".git",
    ".gradle",
    ".npm",
    ".nvm",
    ".openclaw",
    ".pyenv",
    ".rustup",
    ".Trash",
    ".venv",
    "__pycache__",
    "Applications",
    "Applications (Parallels)",
    "Caches",
    "DerivedData",
    "Library",
    "Movies",
    "Music",
    "node_modules",
    "Parallels",
    "Pictures",
    "venv",
}


def redact_url(value: str) -> str:
    if "://" not in value:
        if "@" in value and ":" in value.split("@", 1)[1]:
            return value.split("@", 1)[1]
        return value
    parts = urlsplit(value)
    netloc = parts.netloc.split("@", 1)[-1]
    return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))


def repo_slug_from_url(value: str) -> str:
    redacted = redact_url(value).removesuffix("/")
    if "://" in redacted:
        path = urlsplit(redacted).path.strip("/")
    elif ":" in redacted:
        path = redacted.split(":", 1)[1].strip("/")
    else:
        path = redacted.strip("/")
    return path.removesuffix(".git")


def classify_remote(url: str, gitea_fragment: str) -> str:
    lowered = url.lower()
    if gitea_fragment.lower() in lowered:
        return "gitea"
    if "github.com" in lowered:
        return "github"
    if "192.168.0.110:8929" in lowered:
        return "gitlab_110"
    if "192.168.0.110" in lowered:
        return "internal_git_110"
    return "other"


def git_config_path(repo_path: Path) -> Path | None:
    git_path = repo_path / ".git"
    if git_path.is_dir():
        config_path = git_path / "config"
        return config_path if config_path.exists() else None
    if not git_path.is_file():
        return None

    text = git_path.read_text(encoding="utf-8", errors="replace")
    for line in text.splitlines():
        if line.startswith("gitdir:"):
            raw_gitdir = line.split(":", 1)[1].strip()
            gitdir = Path(raw_gitdir)
            if not gitdir.is_absolute():
                gitdir = (repo_path / gitdir).resolve()
            config_path = gitdir / "config"
            return config_path if config_path.exists() else None
    return None


def remote_name(section: str) -> str | None:
    prefix = 'remote "'
    if section.startswith(prefix) and section.endswith('"'):
        return section[len(prefix) : -1]
    return None


def read_remotes(repo_path: Path, gitea_fragment: str) -> list[dict[str, str]]:
    config_path = git_config_path(repo_path)
    if config_path is None:
        return []

    parser = configparser.RawConfigParser(strict=False)
    parser.read(config_path, encoding="utf-8")
    remotes: list[dict[str, str]] = []
    for section in parser.sections():
        name = remote_name(section)
        if not name or not parser.has_option(section, "url"):
            continue
        raw_url = parser.get(section, "url").strip()
        redacted_url = redact_url(raw_url)
        remotes.append(
            {
                "name": name,
                "kind": classify_remote(redacted_url, gitea_fragment),
                "url_redacted": redacted_url,
                "repo_slug": repo_slug_from_url(redacted_url),
            }
        )
    return remotes


def should_skip_dir(path: Path, root: Path, max_depth: int, exclude_names: set[str]) -> bool:
    if path.name in exclude_names:
        return True
    try:
        depth = len(path.relative_to(root).parts)
    except ValueError:
        return True
    return depth > max_depth


def find_repos(roots: list[Path], max_depth: int, exclude_names: set[str]) -> list[Path]:
    repos: dict[str, Path] = {}
    for root in roots:
        if not root.exists():
            continue
        for current, dirs, _files in os.walk(root):
            current_path = Path(current)
            if should_skip_dir(current_path, root, max_depth, exclude_names):
                dirs[:] = []
                continue
            if (current_path / ".git").exists():
                repos[str(current_path.resolve())] = current_path.resolve()
                dirs[:] = []
                continue
            dirs[:] = [
                name
                for name in dirs
                if not should_skip_dir(current_path / name, root, max_depth, exclude_names)
            ]
    return sorted(repos.values(), key=lambda path: str(path))


def summarize_repo(repo_path: Path, remotes: list[dict[str, str]]) -> dict[str, object]:
    gitea = [remote["repo_slug"] for remote in remotes if remote["kind"] == "gitea"]
    github = [remote["repo_slug"] for remote in remotes if remote["kind"] == "github"]
    internal_110 = [
        remote["repo_slug"]
        for remote in remotes
        if remote["kind"] in ("internal_git_110", "gitlab_110")
    ]
    if gitea and github:
        status = "mapped"
    elif gitea:
        status = "gitea_only_local"
    elif github:
        status = "github_only_local"
    elif internal_110:
        status = "internal_110_only"
    else:
        status = "other_remote"
    return {
        "repo_path": str(repo_path),
        "repo_name": repo_path.name,
        "status": status,
        "gitea_repos": sorted(set(gitea)),
        "github_repos": sorted(set(github)),
        "internal_110_repos": sorted(set(internal_110)),
        "remotes": remotes,
    }


def build_inventory(
    roots: list[Path],
    max_depth: int,
    exclude_names: set[str],
    gitea_fragment: str,
) -> dict[str, object]:
    repo_paths = find_repos(roots, max_depth, exclude_names)
    repos = [
        summarize_repo(repo_path, read_remotes(repo_path, gitea_fragment))
        for repo_path in repo_paths
    ]
    gitea_linked = [repo for repo in repos if repo["gitea_repos"]]
    github_linked = [repo for repo in repos if repo["github_repos"]]
    mapped = [repo for repo in repos if repo["status"] == "mapped"]
    gitea_only = [repo for repo in repos if repo["status"] == "gitea_only_local"]
    github_only = [repo for repo in repos if repo["status"] == "github_only_local"]
    internal_110 = [repo for repo in repos if repo["status"] == "internal_110_only"]
    unique_gitea = sorted(
        {
            item
            for repo in repos
            for item in repo.get("gitea_repos", [])
            if isinstance(item, str)
        }
    )
    unique_github = sorted(
        {
            item
            for repo in repos
            for item in repo.get("github_repos", [])
            if isinstance(item, str)
        }
    )
    unique_internal_110 = sorted(
        {
            item
            for repo in repos
            for item in repo.get("internal_110_repos", [])
            if isinstance(item, str)
        }
    )
    return {
        "schema_version": "local_git_remote_inventory_v1",
        "status": "partial" if repos else "empty",
        "roots": [str(root) for root in roots],
        "max_depth": max_depth,
        "gitea_host_fragment": gitea_fragment,
        "repo_count": len(repos),
        "gitea_linked_count": len(gitea_linked),
        "github_linked_count": len(github_linked),
        "mapped_count": len(mapped),
        "gitea_only_count": len(gitea_only),
        "github_only_count": len(github_only),
        "internal_110_only_count": len(internal_110),
        "unique_gitea_repo_count": len(unique_gitea),
        "unique_github_repo_count": len(unique_github),
        "unique_internal_110_repo_count": len(unique_internal_110),
        "unique_gitea_repos": unique_gitea,
        "unique_github_repos": unique_github,
        "unique_internal_110_repos": unique_internal_110,
        "repos": repos,
    }


def write_markdown(inventory: dict[str, object], path: Path) -> None:
    lines = [
        "# 本機 Git Remote 盤點快照",
        "",
        "| 項目 | 值 |",
        "|------|----|",
        f"| 狀態 | `{inventory['status']}` |",
        f"| 掃描 root | `{', '.join(inventory['roots'])}` |",
        f"| max depth | `{inventory['max_depth']}` |",
        f"| Gitea host fragment | `{inventory['gitea_host_fragment']}` |",
        f"| repo 數量 | `{inventory['repo_count']}` |",
        f"| Gitea linked | `{inventory['gitea_linked_count']}` |",
        f"| GitHub linked | `{inventory['github_linked_count']}` |",
        f"| mapped | `{inventory['mapped_count']}` |",
        f"| Gitea-only local | `{inventory['gitea_only_count']}` |",
        f"| GitHub-only local | `{inventory['github_only_count']}` |",
        f"| Internal 110-only local | `{inventory['internal_110_only_count']}` |",
        f"| 去重後 Gitea repo | `{inventory['unique_gitea_repo_count']}` |",
        f"| 去重後 GitHub repo | `{inventory['unique_github_repo_count']}` |",
        f"| 去重後 110 內部 repo | `{inventory['unique_internal_110_repo_count']}` |",
        "",
        "## Repo 對照",
        "",
        "| 狀態 | 本機路徑 | Gitea repo | GitHub repo | 110 內部 remote |",
        "|------|----------|------------|-------------|----------------|",
    ]
    repos = inventory.get("repos")
    if isinstance(repos, list):
        for repo in repos:
            if not isinstance(repo, dict):
                continue
            gitea = ", ".join(f"`{item}`" for item in repo.get("gitea_repos", [])) or "-"
            github = ", ".join(f"`{item}`" for item in repo.get("github_repos", [])) or "-"
            internal_110 = (
                ", ".join(f"`{item}`" for item in repo.get("internal_110_repos", [])) or "-"
            )
            lines.append(
                "| "
                + " | ".join(
                    [
                        f"`{repo.get('status', '')}`",
                        f"`{repo.get('repo_path', '')}`",
                        gitea,
                        github,
                        internal_110,
                    ]
                )
                + " |"
            )
    lines.extend(
        [
            "",
            "> 注意：本檔只代表本機指定 roots 可見的 Git working tree，不等同 Gitea server 全量 repo 清單。",
            "> 輸出前已移除 remote URL 中的 username、password、token。",
            "",
        ]
    )
    path.write_text("\n".join(lines), encoding="utf-8")


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--root", action="append", required=True)
    parser.add_argument("--max-depth", type=int, default=4)
    parser.add_argument("--exclude-name", action="append", default=[])
    parser.add_argument("--gitea-host-fragment", default="192.168.0.110:3001")
    parser.add_argument("--output-json", required=True)
    parser.add_argument("--output-md", required=True)
    args = parser.parse_args()

    roots = [Path(root).expanduser().resolve() for root in args.root]
    exclude_names = set(DEFAULT_EXCLUDE_NAMES)
    exclude_names.update(args.exclude_name)
    inventory = build_inventory(
        roots=roots,
        max_depth=args.max_depth,
        exclude_names=exclude_names,
        gitea_fragment=args.gitea_host_fragment,
    )
    Path(args.output_json).write_text(
        json.dumps(inventory, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    write_markdown(inventory, Path(args.output_md))
    if inventory["status"] == "empty":
        print("沒有找到本機 Git working tree", file=sys.stderr)
        return 2
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
