# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
"""Internal team roster: generation and lookup.

This module provides two main capabilities:
1. **Generation** (used by CI cron job): Fetches Slack and GitHub org member data,
   cross-references by email, and produces a sorted JSON roster artifact.
2. **Lookup** (used by MCP tools at runtime): Fetches the latest roster artifact
   from GitHub Actions and searches it by any key field.

The roster artifact is a JSON array of person records sorted by Slack display name.
Each record contains Slack and GitHub identity fields joined by email address.
"""

from __future__ import annotations

import argparse
import io
import json
import os
import sys
import time
import zipfile
from datetime import datetime, timezone

import requests
from slack_sdk import WebClient

from airbyte_ops_mcp.github_api import GITHUB_API_BASE, resolve_github_token

ROSTER_REPO_OWNER = "airbytehq"
ROSTER_REPO_NAME = "airbyte-ops-mcp"
ROSTER_WORKFLOW_NAME = "internal_team_roster.yml"
ROSTER_ARTIFACT_NAME = "internal-team-roster"

_ROSTER_CACHE: list[dict[str, str | int | None]] | None = None
_ROSTER_CACHE_TIME: float = 0.0
_ROSTER_CACHE_TTL_SECONDS: float = 3600.0


def _fetch_slack_members(token: str) -> list[dict[str, str | None]]:
    """Fetch all members from a Slack workspace.

    Uses the Slack SDK WebClient with pagination.

    Args:
        token: Slack Bot User OAuth Token with users:read scope.
            If the token also has users:read.email, the email field will be populated.

    Returns:
        List of dicts with keys: slack_id, slack_display_name, slack_real_name, slack_email.
    """
    client = WebClient(token=token)
    members: list[dict[str, str | None]] = []
    cursor: str | None = None

    while True:
        kwargs: dict[str, str | int] = {"limit": 200}
        if cursor:
            kwargs["cursor"] = cursor

        response = client.users_list(**kwargs)

        for member in response.get("members", []):
            if (
                member.get("deleted")
                or member.get("is_bot")
                or member.get("id") == "USLACKBOT"
            ):
                continue

            profile = member.get("profile", {})
            display_name = (
                profile.get("display_name")
                or profile.get("real_name")
                or member.get("name")
                or ""
            )
            real_name = profile.get("real_name") or member.get("real_name") or ""
            members.append(
                {
                    "slack_id": member.get("id"),
                    "slack_display_name": display_name,
                    "slack_real_name": real_name,
                    "slack_email": profile.get("email"),
                }
            )

        cursor = response.get("response_metadata", {}).get("next_cursor")
        if not cursor:
            break

    return members


def _fetch_github_org_members(
    token: str,
    org: str = "airbytehq",
) -> list[dict[str, str | int | None]]:
    """Fetch all members from a GitHub organization.

    Tries /orgs/{org}/members first (requires read:org scope).
    Falls back to /orgs/{org}/public_members if that returns empty
    (works with any valid GitHub token).

    For each member, fetches detailed user info to get name and email.

    Args:
        token: GitHub API token.
        org: GitHub organization name.

    Returns:
        List of dicts with keys: github_id, github_handle, github_name,
        github_public_email.
    """
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28",
    }

    member_logins: list[str] = []
    for endpoint in ["members", "public_members"]:
        page = 1
        while True:
            response = requests.get(
                f"{GITHUB_API_BASE}/orgs/{org}/{endpoint}",
                headers=headers,
                params={"per_page": "100", "page": str(page)},
                timeout=30,
            )
            if response.status_code == 403:
                break
            response.raise_for_status()
            page_members = response.json()

            if not page_members:
                break

            member_logins.extend(m["login"] for m in page_members)
            page += 1

        if member_logins:
            print(
                f"Fetched {len(member_logins)} GitHub org members "
                f"via /orgs/{org}/{endpoint}",
                file=sys.stderr,
            )
            break

    members: list[dict[str, str | int | None]] = []
    for login in member_logins:
        user_response = requests.get(
            f"{GITHUB_API_BASE}/users/{login}",
            headers=headers,
            timeout=30,
        )
        user_response.raise_for_status()
        user_data = user_response.json()

        members.append(
            {
                "github_id": user_data.get("id"),
                "github_handle": user_data.get("login"),
                "github_name": user_data.get("name"),
                "github_public_email": user_data.get("email"),
            }
        )

    return members


def _normalize_name(name: str) -> str:
    """Normalize a name for fuzzy comparison."""
    return " ".join(name.lower().split())


def generate_roster(
    slack_token: str,
    github_token: str,
    github_org: str = "airbytehq",
) -> list[dict[str, str | int | None]]:
    """Generate the internal team roster by joining Slack and GitHub data.

    Cross-references Slack members and GitHub org members using:
    1. Email matching (if Slack token has users:read.email scope)
    2. Name-based matching as fallback (Slack real_name/display_name vs GitHub name)

    Returns a list of person records sorted by tier then name:
    1. Matched across both Slack and GitHub
    2. Slack members with @airbyte.io email
    3. GitHub members with @airbyte.io email
    4. Everyone else

    Args:
        slack_token: Slack Bot User OAuth Token.
        github_token: GitHub API token.
        github_org: GitHub organization name.

    Returns:
        List of person dicts sorted by slack_display_name.
    """
    slack_members = _fetch_slack_members(slack_token)
    github_members = _fetch_github_org_members(github_token, org=github_org)

    github_by_email: dict[str, dict[str, str | int | None]] = {}
    github_by_name: dict[str, dict[str, str | int | None]] = {}
    for gh_member in github_members:
        email = gh_member.get("github_public_email")
        if email and isinstance(email, str):
            github_by_email[email.lower()] = gh_member
        gh_name = gh_member.get("github_name")
        if gh_name and isinstance(gh_name, str):
            normalized = _normalize_name(gh_name)
            if normalized not in github_by_name:
                github_by_name[normalized] = gh_member

    roster: list[dict[str, str | int | None]] = []
    matched_github_ids: set[int | str] = set()
    email_matches = 0
    name_matches = 0

    for slack_member in slack_members:
        record: dict[str, str | int | None] = {
            "slack_id": slack_member.get("slack_id"),
            "slack_display_name": slack_member.get("slack_display_name"),
            "slack_email": slack_member.get("slack_email"),
            "github_id": None,
            "github_handle": None,
            "github_public_email": None,
        }

        gh_match: dict[str, str | int | None] | None = None

        slack_email = slack_member.get("slack_email")
        if slack_email and isinstance(slack_email, str):
            gh_match = github_by_email.get(slack_email.lower())
            if gh_match:
                email_matches += 1

        if gh_match is None:
            slack_real_name = slack_member.get("slack_real_name", "")
            slack_display = slack_member.get("slack_display_name", "")
            if slack_real_name and isinstance(slack_real_name, str):
                gh_match = github_by_name.get(_normalize_name(slack_real_name))
            if gh_match is None and slack_display and isinstance(slack_display, str):
                gh_match = github_by_name.get(_normalize_name(slack_display))
            if gh_match:
                name_matches += 1

        if gh_match:
            record["github_id"] = gh_match.get("github_id")
            record["github_handle"] = gh_match.get("github_handle")
            record["github_public_email"] = gh_match.get("github_public_email")
            gh_id = gh_match.get("github_id")
            if gh_id is not None:
                matched_github_ids.add(gh_id)

        roster.append(record)

    for gh_member in github_members:
        gh_id = gh_member.get("github_id")
        if gh_id is not None and gh_id in matched_github_ids:
            continue
        roster.append(
            {
                "slack_id": None,
                "slack_display_name": None,
                "slack_email": None,
                "github_id": gh_member.get("github_id"),
                "github_handle": gh_member.get("github_handle"),
                "github_public_email": gh_member.get("github_public_email"),
            }
        )

    def _sort_key(r: dict[str, str | int | None]) -> tuple[int, str]:
        has_slack = r.get("slack_id") is not None
        has_github = r.get("github_handle") is not None
        slack_email = str(r.get("slack_email") or "")
        gh_email = str(r.get("github_public_email") or "")
        is_airbyte_slack = slack_email.lower().endswith("@airbyte.io")
        is_airbyte_gh = gh_email.lower().endswith("@airbyte.io")

        if has_slack and has_github:
            tier = 0
        elif has_slack and is_airbyte_slack:
            tier = 1
        elif has_github and is_airbyte_gh:
            tier = 2
        else:
            tier = 3

        name = (r.get("slack_display_name") or r.get("github_handle") or "").lower()
        return (tier, name)

    roster.sort(key=_sort_key)

    print(
        f"Roster matching stats: {email_matches} by email, "
        f"{name_matches} by name, "
        f"{len(slack_members)} Slack members, "
        f"{len(github_members)} GitHub members",
        file=sys.stderr,
    )

    return roster


def _download_latest_artifact(
    owner: str = ROSTER_REPO_OWNER,
    repo: str = ROSTER_REPO_NAME,
    workflow_name: str = ROSTER_WORKFLOW_NAME,
    artifact_name: str = ROSTER_ARTIFACT_NAME,
    token: str | None = None,
) -> list[dict[str, str | int | None]]:
    """Download and parse the latest roster artifact from GitHub Actions.

    Finds the most recent successful workflow run and downloads the artifact.

    Args:
        owner: Repository owner.
        repo: Repository name.
        workflow_name: Workflow file name.
        artifact_name: Name of the artifact to download.
        token: GitHub API token. If None, resolved from environment.

    Returns:
        Parsed roster data (list of person dicts).

    Raises:
        RuntimeError: If no artifact is found or download fails.
    """
    if token is None:
        token = resolve_github_token(preferred_env_vars=["GITHUB_TOKEN"])

    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28",
    }

    runs_url = (
        f"{GITHUB_API_BASE}/repos/{owner}/{repo}/actions/workflows/{workflow_name}/runs"
    )
    runs_response = requests.get(
        runs_url,
        headers=headers,
        params={"status": "success", "per_page": "1", "branch": "main"},
        timeout=30,
    )
    runs_response.raise_for_status()
    runs_data = runs_response.json()

    workflow_runs = runs_data.get("workflow_runs", [])
    if not workflow_runs:
        raise RuntimeError(
            f"No successful runs found for workflow '{workflow_name}' "
            f"in {owner}/{repo}."
        )

    run_id = workflow_runs[0]["id"]

    artifacts_url = (
        f"{GITHUB_API_BASE}/repos/{owner}/{repo}/actions/runs/{run_id}/artifacts"
    )
    artifacts_response = requests.get(artifacts_url, headers=headers, timeout=30)
    artifacts_response.raise_for_status()
    artifacts_data = artifacts_response.json()

    target_artifact = None
    for artifact in artifacts_data.get("artifacts", []):
        if artifact["name"] == artifact_name:
            target_artifact = artifact
            break

    if target_artifact is None:
        raise RuntimeError(
            f"Artifact '{artifact_name}' not found in run {run_id} of {owner}/{repo}."
        )

    download_url = target_artifact["archive_download_url"]
    download_response = requests.get(download_url, headers=headers, timeout=60)
    download_response.raise_for_status()

    with zipfile.ZipFile(io.BytesIO(download_response.content)) as zf:
        names = zf.namelist()
        json_files = [n for n in names if n.endswith(".json")]
        if not json_files:
            raise RuntimeError(f"No JSON files found in artifact '{artifact_name}'.")
        with zf.open(json_files[0]) as f:
            data = json.loads(f.read())

    if isinstance(data, dict) and "members" in data:
        return data["members"]
    if isinstance(data, list):
        return data
    raise RuntimeError(
        f"Unexpected artifact format in '{artifact_name}': "
        f"expected a list or dict with 'members' key."
    )


def fetch_roster(
    token: str | None = None,
) -> list[dict[str, str | int | None]]:
    """Fetch the latest internal team roster from the CI artifact.

    Uses an in-memory cache with a 1-hour TTL to avoid re-downloading
    the artifact on every call.

    Args:
        token: GitHub API token. If None, resolved from environment.

    Returns:
        List of person dicts sorted by slack_display_name.
    """
    global _ROSTER_CACHE, _ROSTER_CACHE_TIME

    now = time.monotonic()
    if (
        _ROSTER_CACHE is not None
        and (now - _ROSTER_CACHE_TIME) < _ROSTER_CACHE_TTL_SECONDS
    ):
        return _ROSTER_CACHE

    roster = _download_latest_artifact(token=token)
    _ROSTER_CACHE = roster
    _ROSTER_CACHE_TIME = now
    return roster


def search_roster(
    roster: list[dict[str, str | int | None]],
    query: str,
) -> list[dict[str, str | int | None]]:
    """Search the roster by any field value.

    Performs case-insensitive partial matching against all string fields
    and exact matching against numeric fields (github_id).

    Args:
        roster: The roster data (list of person dicts).
        query: Search string to match against any field.

    Returns:
        List of matching person records.
    """
    query_lower = query.lower().strip()
    results: list[dict[str, str | int | None]] = []

    for person in roster:
        for _key, value in person.items():
            if value is None:
                continue
            if isinstance(value, str) and query_lower in value.lower():
                results.append(person)
                break
            if isinstance(value, int) and query_lower == str(value):
                results.append(person)
                break

    return results


def _cli_generate(output_path: str) -> None:
    """CLI entrypoint for roster generation (used by CI workflow)."""
    slack_token = os.environ.get("SLACK_HYDRA_BOT_TOKEN") or os.environ.get(
        "SLACK_AIRBYTE_TEAM_READ_USERS"
    )
    if not slack_token:
        print(
            "Error: SLACK_HYDRA_BOT_TOKEN or SLACK_AIRBYTE_TEAM_READ_USERS "
            "environment variable is required.",
            file=sys.stderr,
        )
        sys.exit(1)

    github_token = os.environ.get("GITHUB_TOKEN")
    if not github_token:
        github_token = resolve_github_token(preferred_env_vars=["GITHUB_TOKEN"])

    roster = generate_roster(
        slack_token=slack_token,
        github_token=github_token,
    )

    output = {
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "count": len(roster),
        "members": roster,
    }

    with open(output_path, "w") as f:
        json.dump(output, f, indent=2)

    print(
        f"Roster generated: {len(roster)} members written to {output_path}",
        file=sys.stderr,
    )


def main() -> None:
    """Main CLI entrypoint for the internal team roster module."""
    parser = argparse.ArgumentParser(
        description="Internal team roster generator and lookup."
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    gen_parser = subparsers.add_parser(
        "generate", help="Generate roster artifact from Slack and GitHub APIs."
    )
    gen_parser.add_argument(
        "--output", required=True, help="Output file path for the roster JSON."
    )

    args = parser.parse_args()

    if args.command == "generate":
        _cli_generate(args.output)


if __name__ == "__main__":
    main()
