"""Push server checkouts to GitHub using a fresh GitHub App installation token.

Plain ``git push`` fails: origin stores a one-hour ``x-access-token`` that is
already expired. This script mints a new token, rewrites origin, optionally
commits leftover Cloud-runtime files, and pushes. It never prints the token.

Usage (P2HS-BE root, same .env as the API):

    python -m scripts.push_checkouts_to_github
    python -m scripts.push_checkouts_to_github --apply
    python -m scripts.push_checkouts_to_github --apply --project-id <uuid>
"""
from __future__ import annotations  # noqa: I001

import argparse
import asyncio
import re
from pathlib import Path
from uuid import UUID

from src.core.orm_import import import_all_orm_models

import_all_orm_models()

from src.config import get_settings  # noqa: E402
from src.core.database import async_session_factory  # noqa: E402
from src.domain.projects.models import Project  # noqa: E402
from src.domain.projects.paths import get_project_repo_path  # noqa: E402

_SECRET_PATHS = (
    ".env",
    ".env.local",
    ".env.production",
    ".dev.vars",
    "node_modules",
    ".nuxt",
    ".output",
    ".p2hs/redeploy-build.log",
)


def redact_remote(url: str) -> str:
    return re.sub(r"(x-access-token:)[^@/]+", r"\1***", url, flags=re.I)


def _parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Refresh GitHub App origin tokens and push local checkouts.",
    )
    parser.add_argument(
        "--apply",
        action="store_true",
        help="Commit leftover tracked files if needed, refresh origin, and push.",
    )
    parser.add_argument("--project-id", action="append", help="Limit to one Core project UUID.")
    parser.add_argument(
        "--no-commit",
        action="store_true",
        help="Do not create a commit; only refresh origin and push existing commits.",
    )
    parser.add_argument(
        "--force-with-lease",
        action="store_true",
        help="Push with --force-with-lease (default is a normal fast-forward push).",
    )
    return parser.parse_args()


async def _installation_clone_url(project: Project) -> str:
    from src.integrations.github_client import GitHubAppClient
    from src.integrations.github_installation import resolve_installation_id_for_workspace

    linked = getattr(project, "repo", None)
    if linked is None or not getattr(linked, "repo_full_name", None):
        raise RuntimeError("no GitHub repo linked")
    installation_id = await resolve_installation_id_for_workspace(
        async_session_factory,
        project.workspace_id,
    )
    github = GitHubAppClient(installation_id=installation_id)
    return await github.get_authenticated_clone_url(linked.repo_full_name)


async def _set_origin_url(repo_path: str, clone_url: str) -> None:
    from src.workers.repo_ops import run_git

    rc, _, _ = await run_git(repo_path, "remote", "get-url", "origin")
    if rc == 0:
        rc, _, err = await run_git(repo_path, "remote", "set-url", "origin", clone_url)
        if rc:
            raise RuntimeError(f"git remote set-url failed: {err.strip()}")
        return
    rc, _, err = await run_git(repo_path, "remote", "add", "origin", clone_url)
    if rc:
        raise RuntimeError(f"git remote add failed: {err.strip()}")


async def _unstage_secrets(repo_path: str) -> None:
    from src.workers.repo_ops import run_git

    await run_git(repo_path, "reset", "-q", "HEAD", "--", *_SECRET_PATHS)


async def _commit_if_dirty(repo_path: str, *, allow_commit: bool) -> bool:
    from src.config import get_settings
    from src.workers.repo_ops import run_git

    rc, porcelain, err = await run_git(repo_path, "status", "--porcelain")
    if rc:
        raise RuntimeError(f"git status failed: {err.strip()}")
    if not (porcelain or "").strip():
        return False
    if not allow_commit:
        raise RuntimeError("working tree is dirty; rerun without --no-commit")
    settings = get_settings()
    for cmd in (
        ("config", "user.name", settings.git_committer_name),
        ("config", "user.email", settings.git_committer_email),
        ("add", "-A"),
    ):
        code, _, fail = await run_git(repo_path, *cmd)
        if code:
            raise RuntimeError(f"git {cmd[0]} failed: {fail.strip()}")
    await _unstage_secrets(repo_path)
    code, _, fail = await run_git(
        repo_path,
        "commit",
        "-m",
        "chore: sync server Cloud runtime checkout\n\nAuthored by P2HS only",
    )
    if code:
        if "nothing to commit" in (fail or "").lower():
            return False
        raise RuntimeError(f"git commit failed: {fail.strip()}")
    return True


async def _push_current_branch(repo_path: str, *, force_with_lease: bool) -> str:
    from src.workers.repo_ops import push_repo, run_git

    rc, branch, err = await run_git(repo_path, "branch", "--show-current")
    name = (branch or "").strip()
    if rc or not name:
        raise RuntimeError(f"checkout is not on a branch: {err.strip()}")
    await push_repo(repo_path, branch=name, force_with_lease=force_with_lease)
    return name


async def _list_projects(project_ids: list[UUID]) -> list[Project]:
    from src.domain.projects.repository import ProjectRepository

    async with async_session_factory() as db:
        return await ProjectRepository(db).list_pages_projects_with_repos(project_ids or None)


async def run(args: argparse.Namespace) -> int:
    settings = get_settings()
    ids = [UUID(value) for value in (args.project_id or [])]
    projects = await _list_projects(ids)
    print(f"Found {len(projects)} HTML/React/Nuxt project(s).")
    pushed = skipped = errors = 0
    for project in projects:
        repo = Path(
            get_project_repo_path(settings.repo_workspace, project.workspace_id, project.id)
        )
        linked = getattr(project, "repo", None)
        remote = getattr(linked, "repo_full_name", None) if linked is not None else None
        label = f"  {project.name!r}  ({project.id})  github={remote or '-'}"
        print(label, end=" ", flush=True)
        if not remote:
            print("[skip: no GitHub repo]")
            skipped += 1
            continue
        if not (repo / ".git").is_dir():
            print("[skip: checkout is not a git repo]")
            skipped += 1
            continue
        if not args.apply:
            print(f"[dry-run checkout={repo}]")
            skipped += 1
            continue
        try:
            clone_url = await _installation_clone_url(project)
            await _set_origin_url(str(repo), clone_url)
            committed = await _commit_if_dirty(str(repo), allow_commit=not args.no_commit)
            branch = await _push_current_branch(
                str(repo),
                force_with_lease=args.force_with_lease,
            )
            extra = " committed," if committed else ""
            print(f"[pushed {branch}{extra} github={remote}]")
            pushed += 1
        except Exception as exc:
            print(f"[ERROR: {exc}]")
            errors += 1
    print(f"\nDone. pushed={pushed}  skipped={skipped}  errors={errors}")
    return 1 if errors else 0


def main() -> None:
    raise SystemExit(asyncio.run(run(_parse_args())))


if __name__ == "__main__":
    main()
