#!/usr/bin/env python
"""Regenerate and push a project's npm lockfile through Core's GitHub setup.

Usage from the Core repository root:

    python -m scripts.repair_project_lockfile <project_uuid> --branch preview
    python -m scripts.repair_project_lockfile <project_uuid> --branch main

The script deliberately changes and commits only ``package-lock.json``.  It
uses the same GitHub App authentication and managed project workspace as the
worker, so no personal GitHub token or manual git credentials are required.
"""
from __future__ import annotations

import argparse
import asyncio
import os
import sys
from uuid import UUID

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from src.core.orm_import import import_all_orm_models

import_all_orm_models()


async def _run_command(repo_path: str, *args: str) -> tuple[int, str, str]:
    proc = await asyncio.create_subprocess_exec(
        *args,
        cwd=repo_path,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    stdout, stderr = await proc.communicate()
    return (
        proc.returncode,
        (stdout or b"").decode(errors="replace"),
        (stderr or b"").decode(errors="replace"),
    )


async def _run_npm(repo_path: str, *args: str) -> None:
    proc = await asyncio.create_subprocess_exec(
        "npm",
        *args,
        cwd=repo_path,
    )
    rc = await proc.wait()
    if rc != 0:
        raise RuntimeError(f"npm {' '.join(args)} failed with exit code {rc}")


async def repair_project_lockfile(project_id: str, branch: str) -> int:
    from src.config import get_settings
    from src.core.database import async_session_factory
    from src.domain.projects.repository import ProjectRepository, ProjectRepoRepository
    from src.integrations.github_client import GitHubAppClient
    from src.integrations.github_installation import resolve_installation_id_for_workspace
    from src.workers.repo_ops import get_project_repo_path, prepare_working_tree, push_repo

    pid = UUID(project_id)
    settings = get_settings()

    async with async_session_factory() as db:
        project = await ProjectRepository(db).get_by_id(pid)
        if not project or project.deleted_at is not None:
            print(f"Project not found or deleted: {project_id}", file=sys.stderr)
            return 1

        project_repo = await ProjectRepoRepository(db).get_by_project_id(pid)
        if not project_repo or not project_repo.repo_full_name:
            print("Project has no linked GitHub repository.", file=sys.stderr)
            return 1

        workspace_id = project.workspace_id
        repo_full_name = project_repo.repo_full_name
        default_branch = project_repo.default_branch or "main"

    if branch == "preview" and not getattr(project_repo, "preview_branch", None):
        print("Project has no configured preview branch.", file=sys.stderr)
        return 1

    installation_id = await resolve_installation_id_for_workspace(
        async_session_factory,
        workspace_id,
    )
    github = GitHubAppClient(installation_id=installation_id)
    clone_url = await github.get_authenticated_clone_url(repo_full_name)

    repo_path = get_project_repo_path(
        settings.repo_workspace,
        workspace_id,
        pid,
    )

    print(f"Project : {pid}")
    print(f"Repo    : {repo_full_name}")
    print(f"Branch  : {branch}")
    print(f"Path    : {repo_path}")

    await prepare_working_tree(
        repo_path,
        clone_url,
        branch=branch,
        heal_missing_remote_from=default_branch if branch != default_branch else None,
    )

    lockfile = os.path.join(repo_path, "package-lock.json")
    package_json = os.path.join(repo_path, "package.json")
    if not os.path.isfile(package_json):
        print("package.json is missing; this is not an npm project.", file=sys.stderr)
        return 1
    if not os.path.isfile(lockfile):
        print("package-lock.json is missing; refusing to create a new lockfile.", file=sys.stderr)
        return 1

    print("Regenerating package-lock.json ...")
    await _run_npm(repo_path, "install", "--package-lock-only", "--ignore-scripts")

    print("Verifying with npm ci ...")
    await _run_npm(repo_path, "ci", "--ignore-scripts")

    rc, changed, err = await _run_command(repo_path, "git", "status", "--porcelain")
    if rc != 0:
        raise RuntimeError(f"git status failed: {err.strip()}")

    changed_files = [line[3:] for line in changed.splitlines() if len(line) >= 4]
    unexpected = [path for path in changed_files if path != "package-lock.json"]
    if unexpected:
        raise RuntimeError(
            "Refusing to commit unexpected files: " + ", ".join(unexpected)
        )

    if "package-lock.json" not in changed_files:
        print("package-lock.json is already synchronized; nothing to commit.")
        return 0

    await _run_command(repo_path, "git", "config", "user.name", settings.git_committer_name)
    await _run_command(repo_path, "git", "config", "user.email", settings.git_committer_email)

    rc, _, err = await _run_command(repo_path, "git", "add", "package-lock.json")
    if rc != 0:
        raise RuntimeError(f"git add failed: {err.strip()}")

    rc, _, err = await _run_command(
        repo_path,
        "git",
        "commit",
        "-m",
        "chore: synchronize npm lockfile",
    )
    if rc != 0:
        raise RuntimeError(f"git commit failed: {err.strip()}")

    await push_repo(
        repo_path,
        branch,
        force_with_lease=branch != default_branch,
    )
    print(f"Committed and pushed package-lock.json to {repo_full_name}:{branch}")
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("project_id", help="P2HS project UUID")
    parser.add_argument(
        "--branch",
        default="preview",
        help="Branch to repair: preview or main (default: preview)",
    )
    args = parser.parse_args()
    if args.branch not in {"main", "preview"}:
        parser.error("--branch must be main or preview")
    return asyncio.run(repair_project_lockfile(args.project_id, args.branch))


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