#!/usr/bin/env python3

import argparse
import json
import subprocess
import sys
from collections import Counter
from datetime import date, datetime, timezone
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo


PR_QUERY = """
query($owner:String!, $name:String!, $cursor:String) {
  repository(owner:$owner, name:$name) {
    pullRequests(
      first: 50,
      after: $cursor,
      orderBy: { field: UPDATED_AT, direction: DESC },
      states: [OPEN, CLOSED, MERGED]
    ) {
      pageInfo { hasNextPage endCursor }
      nodes {
        number
        title
        url
        createdAt
        updatedAt
        mergedAt
        author { login }
        mergedBy { login }
        reviews(first: 100) {
          nodes {
            state
            submittedAt
            author { login }
            url
          }
        }
        comments(first: 100) {
          nodes {
            createdAt
            author { login }
            url
          }
        }
      }
    }
  }
}
"""


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Build DailySeek GitHub activity report using repo-level GraphQL queries."
    )
    parser.add_argument(
        "--profile",
        required=False,
        help="Profile key from .cursor/dailyseek.json (for example: sebas, shark).",
    )
    parser.add_argument(
        "--date",
        required=False,
        help="Local date in YYYY-MM-DD. Default: today in configured timezone.",
    )
    parser.add_argument(
        "--config",
        required=False,
        help="Override path to dailyseek config JSON.",
    )
    parser.add_argument(
        "--workspace-root",
        required=False,
        help="Explicit workspace root. Preferred when invoked by wrapper scripts.",
    )
    parser.add_argument(
        "--repos-config",
        required=False,
        help="Override path to repos config JSON.",
    )
    parser.add_argument(
        "--max-pages",
        required=False,
        type=int,
        help="Max GraphQL pages per repository.",
    )
    return parser.parse_args()


def read_json(path: Path) -> dict[str, Any]:
    with path.open("r", encoding="utf-8") as file:
        return json.load(file)


def run_gh_graphql(owner: str, name: str, cursor: str | None) -> dict[str, Any]:
    command = [
        "gh",
        "api",
        "graphql",
        "-f",
        f"query={PR_QUERY}",
        "-f",
        f"owner={owner}",
        "-f",
        f"name={name}",
    ]
    if cursor:
        command.extend(["-f", f"cursor={cursor}"])
    process = subprocess.run(command, capture_output=True, text=True, timeout=60)
    if process.returncode != 0:
        raise RuntimeError(process.stderr.strip() or process.stdout.strip())
    return json.loads(process.stdout)


def resolve_workspace_root(explicit_workspace_root: str | None) -> Path:
    if explicit_workspace_root:
        return Path(explicit_workspace_root).resolve()
    return Path.cwd().resolve()


def resolve_members(profile: dict[str, Any]) -> list[dict[str, str]]:
    if isinstance(profile.get("members"), list):
        members = []
        for member in profile["members"]:
            github = member.get("github")
            if not github:
                continue
            members.append(
                {
                    "name": member.get("name") or github,
                    "github": github,
                }
            )
        if members:
            return members

    # Backward compatibility with older profile shape.
    if profile.get("github_user"):
        github = profile["github_user"]
        return [{"name": profile.get("name") or github, "github": github}]

    # Backward compatibility with older profile shape.
    if isinstance(profile.get("github_users"), list):
        return [{"name": github, "github": github} for github in profile["github_users"]]

    return []


def resolve_allowed_repos(
    profile: dict[str, Any], repos_config: dict[str, Any]
) -> tuple[list[str], list[str]]:
    allowed: set[str] = set()
    warnings: list[str] = []

    if isinstance(profile.get("repos"), list):
        allowed.update(profile["repos"])

    repo_keys = profile.get("repo_keys", [])
    repos_map = repos_config.get("repos", {})
    for repo_key in repo_keys:
        repo_item = repos_map.get(repo_key)
        if not repo_item:
            warnings.append(f"repo key not found in repos.json: {repo_key}")
            continue
        github_repo = repo_item.get("github")
        if not github_repo:
            warnings.append(f"repo key without github field in repos.json: {repo_key}")
            continue
        allowed.add(github_repo)

    return sorted(allowed), warnings


def to_local_datetime(timestamp: str, timezone_name: str) -> datetime:
    tz = ZoneInfo(timezone_name)
    utc_dt = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%SZ").replace(
        tzinfo=timezone.utc
    )
    return utc_dt.astimezone(tz)


def add_action(
    actions: list[dict[str, Any]],
    *,
    action_type: str,
    when: datetime,
    repo: str,
    actor_login: str,
    actor_name: str,
    pr_number: int,
    pr_title: str,
    url: str,
) -> None:
    actions.append(
        {
            "type": action_type,
            "when": when,
            "repo": repo,
            "actor_login": actor_login,
            "actor_name": actor_name,
            "pr_number": pr_number,
            "pr_title": pr_title,
            "url": url,
        }
    )


def process_pr_node(
    node: dict[str, Any],
    *,
    repo: str,
    target_date: date,
    timezone_name: str,
    member_logins: set[str],
    member_names: dict[str, str],
    actions: list[dict[str, Any]],
) -> None:
    pr_number = node.get("number")
    pr_title = node.get("title", "")
    pr_url = node.get("url", "")
    if not pr_number:
        return

    created_at = node.get("createdAt")
    author_login = (node.get("author") or {}).get("login")
    if created_at and author_login in member_logins:
        local_dt = to_local_datetime(created_at, timezone_name)
        if local_dt.date() == target_date:
            add_action(
                actions,
                action_type="pr_opened",
                when=local_dt,
                repo=repo,
                actor_login=author_login,
                actor_name=member_names.get(author_login, author_login),
                pr_number=pr_number,
                pr_title=pr_title,
                url=pr_url,
            )

    merged_at = node.get("mergedAt")
    merged_by_login = (node.get("mergedBy") or {}).get("login")
    if merged_at and merged_by_login in member_logins:
        local_dt = to_local_datetime(merged_at, timezone_name)
        if local_dt.date() == target_date:
            add_action(
                actions,
                action_type="pr_merged",
                when=local_dt,
                repo=repo,
                actor_login=merged_by_login,
                actor_name=member_names.get(merged_by_login, merged_by_login),
                pr_number=pr_number,
                pr_title=pr_title,
                url=pr_url,
            )

    review_nodes = ((node.get("reviews") or {}).get("nodes") or [])
    for review in review_nodes:
        review_login = (review.get("author") or {}).get("login")
        review_state = (review.get("state") or "").upper()
        submitted_at = review.get("submittedAt")
        if (
            review_login not in member_logins
            or review_state != "APPROVED"
            or not submitted_at
        ):
            continue
        local_dt = to_local_datetime(submitted_at, timezone_name)
        if local_dt.date() != target_date:
            continue
        add_action(
            actions,
            action_type="review_approved",
            when=local_dt,
            repo=repo,
            actor_login=review_login,
            actor_name=member_names.get(review_login, review_login),
            pr_number=pr_number,
            pr_title=pr_title,
            url=review.get("url") or pr_url,
        )

    comment_nodes = ((node.get("comments") or {}).get("nodes") or [])
    for comment in comment_nodes:
        comment_login = (comment.get("author") or {}).get("login")
        created_at = comment.get("createdAt")
        if comment_login not in member_logins or not created_at:
            continue
        local_dt = to_local_datetime(created_at, timezone_name)
        if local_dt.date() != target_date:
            continue
        add_action(
            actions,
            action_type="comment_created",
            when=local_dt,
            repo=repo,
            actor_login=comment_login,
            actor_name=member_names.get(comment_login, comment_login),
            pr_number=pr_number,
            pr_title=pr_title,
            url=comment.get("url") or pr_url,
        )


def format_action_type(action_type: str) -> str:
    labels = {
        "pr_opened": "PR opened",
        "pr_merged": "PR merged",
        "review_approved": "Review approved",
        "comment_created": "Comment created",
    }
    return labels.get(action_type, action_type)


def main() -> int:
    args = parse_args()
    workspace_root = resolve_workspace_root(args.workspace_root)

    config_path = (
        Path(args.config).resolve()
        if args.config
        else workspace_root / ".cursor" / "dailyseek.json"
    )
    repos_path = (
        Path(args.repos_config).resolve()
        if args.repos_config
        else workspace_root / ".cursor" / "repos.json"
    )

    if not config_path.exists():
        print(f"ERROR: config file not found: {config_path}", file=sys.stderr)
        return 1
    if not repos_path.exists():
        print(f"ERROR: repos config file not found: {repos_path}", file=sys.stderr)
        return 1

    config = read_json(config_path)
    repos_config = read_json(repos_path)

    timezone_name = config.get("timezone", "UTC")
    default_profile = config.get("default_profile")
    profile_key = args.profile or default_profile
    if not profile_key:
        print("ERROR: missing --profile and default_profile in config.", file=sys.stderr)
        return 1

    profiles = config.get("profiles", {})
    profile = profiles.get(profile_key)
    if not profile:
        print(f"ERROR: profile not found in config: {profile_key}", file=sys.stderr)
        return 1

    members = resolve_members(profile)
    if not members:
        print(f"ERROR: profile has no valid members: {profile_key}", file=sys.stderr)
        return 1

    member_logins = {member["github"] for member in members}
    member_names = {member["github"]: member["name"] for member in members}

    max_pages = args.max_pages or config.get("max_pr_pages_per_repo", 8)
    if max_pages <= 0:
        print("ERROR: --max-pages must be greater than 0.", file=sys.stderr)
        return 1

    allowed_repos, repo_warnings = resolve_allowed_repos(profile, repos_config)
    for warning in repo_warnings:
        print(f"WARN: {warning}", file=sys.stderr)
    if not allowed_repos:
        print("ERROR: profile has empty repository scope.", file=sys.stderr)
        return 1

    try:
        timezone_obj = ZoneInfo(timezone_name)
    except Exception:
        print(f"ERROR: invalid timezone in config: {timezone_name}", file=sys.stderr)
        return 1

    if args.date:
        try:
            target_date = datetime.strptime(args.date, "%Y-%m-%d").date()
        except ValueError:
            print("ERROR: invalid --date format. Use YYYY-MM-DD.", file=sys.stderr)
            return 1
    else:
        target_date = datetime.now(timezone_obj).date()

    actions: list[dict[str, Any]] = []
    fetch_errors: list[str] = []

    for repo in allowed_repos:
        owner, name = repo.split("/", 1)
        cursor: str | None = None
        pages = 0

        try:
            while pages < max_pages:
                pages += 1
                response = run_gh_graphql(owner, name, cursor)
                pull_requests = (
                    ((response.get("data") or {}).get("repository") or {}).get(
                        "pullRequests"
                    )
                    or {}
                )
                nodes = pull_requests.get("nodes") or []
                if not nodes:
                    break

                for node in nodes:
                    process_pr_node(
                        node,
                        repo=repo,
                        target_date=target_date,
                        timezone_name=timezone_name,
                        member_logins=member_logins,
                        member_names=member_names,
                        actions=actions,
                    )

                # pullRequests are sorted by updatedAt desc.
                # once a full page is strictly older than target date, stop for this repo.
                page_updated_dates = []
                for node in nodes:
                    updated_at = node.get("updatedAt")
                    if not updated_at:
                        continue
                    page_updated_dates.append(
                        to_local_datetime(updated_at, timezone_name).date()
                    )

                page_info = pull_requests.get("pageInfo") or {}
                has_next = bool(page_info.get("hasNextPage"))
                cursor = page_info.get("endCursor")

                if not has_next:
                    break
                if page_updated_dates and all(d < target_date for d in page_updated_dates):
                    break

        except Exception as error:
            fetch_errors.append(f"{repo}: {error}")

    actions.sort(key=lambda item: item["when"])

    action_type_counts: Counter[str] = Counter()
    repo_counts: Counter[str] = Counter()
    member_counts: Counter[str] = Counter()
    for action in actions:
        action_type_counts[action["type"]] += 1
        repo_counts[action["repo"]] += 1
        member_counts[action["actor_name"]] += 1

    mode = profile.get("mode", "individual")
    member_label = ", ".join(f"{m['name']} ({m['github']})" for m in members)
    repo_scope = ", ".join(allowed_repos)

    print("# DailySeek Activity")
    print("")
    print(f"- Profile: `{profile_key}` ({mode})")
    print(f"- Date: `{target_date.isoformat()}`")
    print(f"- Timezone: `{timezone_name}`")
    print("- Source: `GitHub GraphQL repository pullRequests + reviews + comments`")
    print(
        "- Note: push/create/delete events are intentionally excluded in repo-query mode."
    )
    print(f"- Members: {member_label}")
    print(f"- Repo scope: {repo_scope}")
    print(f"- Actions found: {len(actions)}")
    print("")

    if fetch_errors:
        print("## Fetch warnings")
        for error in fetch_errors:
            print(f"- {error}")
        print("")

    print("## Proved action counters")
    print(f"- PRs opened: {action_type_counts.get('pr_opened', 0)}")
    print(f"- PRs merged: {action_type_counts.get('pr_merged', 0)}")
    print(f"- PR reviews approved: {action_type_counts.get('review_approved', 0)}")
    print(f"- Comments created: {action_type_counts.get('comment_created', 0)}")
    print("")

    print("## Action type counts")
    if action_type_counts:
        for action_type, count in action_type_counts.most_common():
            print(f"- {action_type}: {count}")
    else:
        print("- No actions for selected date/scope.")
    print("")

    print("## Repo counts")
    if repo_counts:
        for repo_name, count in repo_counts.most_common():
            print(f"- {repo_name}: {count}")
    else:
        print("- No repos with actions for selected date/scope.")
    print("")

    if len(members) > 1:
        print("## Member counts")
        if member_counts:
            for member_name, count in member_counts.most_common():
                print(f"- {member_name}: {count}")
        else:
            print("- No member actions for selected date/scope.")
        print("")

    print("## Timeline")
    if not actions:
        print("- No matching actions.")
        return 0

    for action in actions:
        time_str = action["when"].strftime("%H:%M:%S%z")
        action_label = format_action_type(action["type"])
        pr_label = f"PR #{action['pr_number']}: {action['pr_title']}"
        if len(members) > 1:
            prefix = (
                f"- {time_str} | {action['actor_name']} | {action['repo']} | "
                f"{action_label} | {pr_label}"
            )
        else:
            prefix = (
                f"- {time_str} | {action['repo']} | "
                f"{action_label} | {pr_label}"
            )
        if action["url"]:
            print(f"{prefix} | {action['url']}")
        else:
            print(prefix)

    return 0


if __name__ == "__main__":
    sys.exit(main())
