#!/usr/bin/env python3
"""Analyze downstream impact of a dependency upgrade on the CCA codebase.

Usage:
    python generate_impact_analysis.py <package> <current_ver> <latest_ver> <cca_path> <changelog_path>

Prints Markdown impact analysis to stdout. Exits 0 with no output on failure
(never blocks the calling workflow).

Environment variables:
    ANTHROPIC_API_KEY - Required for LLM analysis
"""

import os
import re
import sys
from pathlib import Path

MAX_FILES = 50
MAX_MATCHES = 200
CONTEXT_LINES = 3
MAX_PROMPT_CHARS = 15_000

# Mapping for packages whose import name differs from the normalized package name.
# Only packages in pyproject.toml that need explicit mapping are listed here.
# The fallback (replace hyphens/dots with underscores, lowercase) handles the rest.
PACKAGE_TO_IMPORTS: dict[str, list[str]] = {
    "beautifulsoup4": ["bs4"],
    "Pillow": ["PIL"],
    "google-api-python-client": ["googleapiclient", "apiclient"],
    "google-auth": ["google.auth", "google.oauth2"],
    "pyjwt": ["jwt"],
    "pymupdf": ["fitz"],
    "python-dateutil": ["dateutil"],
    "scikit-learn": ["sklearn"],
    "sklearn-crfsuite": ["sklearn_crfsuite"],
    "selenium-authenticated-proxy": ["selenium_authenticated_proxy"],
    "user-agents": ["user_agents"],
    "websocket-client": ["websocket"],
    "boto3-stubs": ["mypy_boto3"],
    "firebase-admin": ["firebase_admin"],
    "aws-encryption-sdk": ["aws_encryption_sdk"],
    "cerebras_cloud_sdk": ["cerebras.cloud.sdk"],
    "fasttext-numpy2": ["fasttext"],
    "SoundCard": ["soundcard"],
    "more-itertools": ["more_itertools"],
    "yt-dlp": ["yt_dlp"],
    "yt-dlp-ejs": ["yt_dlp_ejs"],
    "langchain-anthropic": ["langchain_anthropic"],
    "langchain-aws": ["langchain_aws"],
    "langchain-cerebras": ["langchain_cerebras"],
    "langchain-community": ["langchain_community"],
    "langchain-core": ["langchain_core"],
    "langchain-google-genai": ["langchain_google_genai"],
    "langchain-groq": ["langchain_groq"],
    "langchain-openai": ["langchain_openai"],
    "langchain-tavily": ["langchain_tavily"],
    "langgraph-checkpoint-mongodb": ["langgraph.checkpoint.mongodb"],
    "langgraph-prebuilt": ["langgraph.prebuilt"],
}

SKIP_DIRS = {"__pycache__", ".git", "node_modules", ".venv", "venv", ".tox", ".mypy_cache", "logs", "data"}


def normalize_name(name: str) -> str:
    """Normalize package name per PEP 503."""
    return re.sub(r"[-_.]+", "-", name).lower()


def resolve_import_names(package_name: str) -> list[str]:
    """Map a PyPI package name to its Python import name(s)."""
    normalized = normalize_name(package_name)
    for key, imports in PACKAGE_TO_IMPORTS.items():
        if normalize_name(key) == normalized:
            return imports
    # Fallback: replace hyphens/dots with underscores, lowercase
    return [re.sub(r"[-.]", "_", package_name).lower()]


def find_usage_in_cca(cca_path: str, import_names: list[str]) -> list[dict]:
    """Find all usage of the given import names in the CCA codebase."""
    matches: list[dict] = []
    files_seen: set[str] = set()

    patterns: list[re.Pattern] = []
    for name in import_names:
        # Direct import: import foo or from foo import ...
        patterns.append(re.compile(rf"^\s*(import\s+{re.escape(name)}\b|from\s+{re.escape(name)}\b)"))
        # For dotted names (e.g., google.auth), also match: from google import auth
        parts = name.split(".")
        if len(parts) > 1:
            parent = ".".join(parts[:-1])
            child = parts[-1]
            patterns.append(re.compile(rf"^\s*from\s+{re.escape(parent)}\s+import\s+.*\b{re.escape(child)}\b"))

    for root, dirs, files in os.walk(cca_path):
        dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
        for fname in files:
            if not fname.endswith(".py"):
                continue
            filepath = os.path.join(root, fname)
            rel_path = os.path.relpath(filepath, cca_path)
            try:
                with open(filepath) as f:
                    lines = f.readlines()
            except Exception:
                continue
            for i, line in enumerate(lines):
                if len(matches) >= MAX_MATCHES:
                    return matches
                for pattern in patterns:
                    if pattern.search(line):
                        files_seen.add(rel_path)
                        start = max(0, i - CONTEXT_LINES)
                        end = min(len(lines), i + CONTEXT_LINES + 1)
                        matches.append({
                            "file": rel_path,
                            "line_number": i + 1,
                            "line": line.rstrip(),
                            "context": [l.rstrip() for l in lines[start:end]],
                        })
                        break  # One match per line
            if len(files_seen) >= MAX_FILES:
                return matches

    return matches


def format_usage_for_prompt(matches: list[dict]) -> str:
    """Format matches into a compact string for the Claude prompt, grouped by file."""
    by_file: dict[str, list[dict]] = {}
    for m in matches:
        by_file.setdefault(m["file"], []).append(m)

    parts: list[str] = []
    for filepath, file_matches in by_file.items():
        parts.append(f"### {filepath}")
        for m in file_matches:
            parts.append(f"Line {m['line_number']}:")
            parts.append("\n".join(f"  {l}" for l in m["context"]))
            parts.append("")

    text = "\n".join(parts)
    if len(text) > MAX_PROMPT_CHARS:
        text = text[:MAX_PROMPT_CHARS] + "\n\n[Usage context truncated]"
    return text


def analyze_with_claude(
    package: str,
    from_ver: str,
    to_ver: str,
    changelog_summary: str,
    usage_context: str,
) -> str:
    """Analyze downstream impact using Claude API."""
    import anthropic

    client = anthropic.Anthropic()
    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system=(
            "You are a dependency upgrade analyst. You assess whether a Python package "
            "upgrade will require code changes in a downstream application. Be specific "
            "and actionable. Use Markdown formatting."
        ),
        messages=[
            {
                "role": "user",
                "content": (
                    f"Package: **{package}**\n"
                    f"Upgrading from `{from_ver}` to `{to_ver}`\n\n"
                    f"### Changelog Summary\n\n{changelog_summary}\n\n"
                    f"### Usage in CCA (downstream application)\n\n{usage_context}\n\n"
                    "Instructions:\n"
                    "- Assess whether any breaking changes or deprecations in the changelog "
                    "affect the specific usage patterns found in CCA\n"
                    "- List any CCA code changes that would be required\n"
                    "- If no changes are needed, say so explicitly\n"
                    "- Rate the risk: **Low** (no changes needed), **Medium** (deprecated APIs "
                    "used but still functional), or **High** (breaking changes affect CCA code)\n"
                    "- Format your response as:\n"
                    "  - **Risk Level**: Low/Medium/High\n"
                    "  - **Assessment**: 1-3 sentences\n"
                    "  - **Required Changes**: bulleted list or 'None'\n"
                    "  - **Files to Review**: list of CCA files that may need attention, or 'None'\n"
                ),
            }
        ],
    )
    return message.content[0].text


def main() -> None:
    if len(sys.argv) != 6:
        print(
            "Usage: generate_impact_analysis.py <package> <current_ver> <latest_ver> <cca_path> <changelog_path>",
            file=sys.stderr,
        )
        sys.exit(1)

    package, current_version, latest_version, cca_path, changelog_path = sys.argv[1:6]

    api_key = os.environ.get("ANTHROPIC_API_KEY")
    if not api_key:
        print("ANTHROPIC_API_KEY not set, skipping impact analysis", file=sys.stderr)
        return

    # Read changelog summary from the previous step
    changelog_summary = ""
    try:
        changelog_summary = Path(changelog_path).read_text().strip()
    except Exception:
        pass
    if not changelog_summary:
        print("No changelog summary available, skipping impact analysis", file=sys.stderr)
        return

    # Verify CCA checkout exists
    if not os.path.isdir(cca_path):
        print(f"CCA directory not found at {cca_path}, skipping impact analysis", file=sys.stderr)
        return

    # Resolve import names
    import_names = resolve_import_names(package)
    print(f"Searching CCA for imports: {import_names}", file=sys.stderr)

    # Find usage in CCA
    matches = find_usage_in_cca(cca_path, import_names)
    if not matches:
        print(f"No usage of {package} found in CCA", file=sys.stderr)
        print(
            f"No direct usage of `{package}` was found in the CCA codebase. "
            "This upgrade is unlikely to require downstream changes."
        )
        return

    unique_files = len(set(m["file"] for m in matches))
    print(f"Found {len(matches)} usage(s) across {unique_files} file(s)", file=sys.stderr)

    usage_context = format_usage_for_prompt(matches)

    # Analyze with Claude
    try:
        analysis = analyze_with_claude(package, current_version, latest_version, changelog_summary, usage_context)
        print(analysis)
    except Exception as e:
        print(f"Failed to analyze with Claude: {e}", file=sys.stderr)


if __name__ == "__main__":
    main()
