"""Claude Code integration for AI-assisted reviews."""

from __future__ import annotations

import shutil
import subprocess
from dataclasses import dataclass
from importlib.resources import files
from pathlib import Path

from jinja2 import BaseLoader, Environment

from .analyze import analyze_module

# Approximate tokens per character (conservative estimate)
CHARS_PER_TOKEN = 4
MAX_TOKENS = 100000  # Leave room for prompt and response
MAX_SOURCE_CHARS = MAX_TOKENS * CHARS_PER_TOKEN


@dataclass
class ChunkInfo:
    """Information about a code chunk for review."""

    content: str
    start_line: int
    end_line: int
    chunk_num: int
    total_chunks: int


def claude_available() -> bool:
    """Check if claude CLI is available."""
    return shutil.which("claude") is not None


def get_priority_description(priority: str) -> str:
    """Get human-readable priority description."""
    descriptions = {
        "critical": "Core functionality - requires thorough review",
        "high": "Important module - careful review recommended",
        "medium": "Standard module - normal review",
        "low": "Low-risk module - basic review sufficient",
    }
    return descriptions.get(priority, "Standard review")


def chunk_source_code(
    source: str, max_chars: int = MAX_SOURCE_CHARS
) -> list[ChunkInfo]:
    """Split source code into chunks if too large.

    Tries to split on function/class boundaries when possible.

    Args:
        source: Source code string.
        max_chars: Maximum characters per chunk.

    Returns:
        List of ChunkInfo objects.
    """
    if len(source) <= max_chars:
        line_count = source.count("\n") + 1
        return [
            ChunkInfo(
                content=source,
                start_line=1,
                end_line=line_count,
                chunk_num=1,
                total_chunks=1,
            )
        ]

    # Split into lines
    lines: list[str] = source.split("\n")
    chunks: list[ChunkInfo] = []
    current_chunk_lines: list[str] = []
    current_chunk_start = 1
    current_chars = 0

    for line in lines:
        line_with_newline = line + "\n"

        # Check if adding this line would exceed limit
        if current_chars + len(line_with_newline) > max_chars and current_chunk_lines:
            # Try to find a good break point (def, class, or blank line)
            break_point = len(current_chunk_lines)

            # Look back for a good break point
            for j in range(
                len(current_chunk_lines) - 1, max(0, len(current_chunk_lines) - 50), -1
            ):
                check_line = current_chunk_lines[j].strip()
                if (
                    check_line.startswith("def ")
                    or check_line.startswith("class ")
                    or check_line.startswith("async def ")
                    or check_line == ""
                ):
                    break_point = j
                    break

            # Save current chunk
            chunk_content = "\n".join(current_chunk_lines[:break_point])
            chunks.append(
                ChunkInfo(
                    content=chunk_content,
                    start_line=current_chunk_start,
                    end_line=current_chunk_start + break_point - 1,
                    chunk_num=len(chunks) + 1,
                    total_chunks=0,  # Will update later
                )
            )

            # Start new chunk with remaining lines
            current_chunk_lines = current_chunk_lines[break_point:]
            current_chunk_start = current_chunk_start + break_point
            current_chars = sum(len(ln) + 1 for ln in current_chunk_lines)

        current_chunk_lines.append(line)
        current_chars += len(line_with_newline)

    # Don't forget the last chunk
    if current_chunk_lines:
        chunks.append(
            ChunkInfo(
                content="\n".join(current_chunk_lines),
                start_line=current_chunk_start,
                end_line=current_chunk_start + len(current_chunk_lines) - 1,
                chunk_num=len(chunks) + 1,
                total_chunks=0,
            )
        )

    # Update total_chunks
    for chunk in chunks:
        chunk.total_chunks = len(chunks)

    return chunks


def build_review_prompt(
    path: str,
    source_code: str,
    module_status,
    config,
    diff: str | None = None,
    chunk_info: ChunkInfo | None = None,
    project_context=None,
    module_context=None,
) -> str:
    """Build the review prompt for Claude.

    Args:
        path: Module path.
        source_code: Source code to review.
        module_status: ModuleStatus object.
        config: Config object.
        diff: Diff since last review (for re-reviews).
        chunk_info: Chunk information if reviewing in parts.
        project_context: ProjectContext object (optional).
        module_context: ModuleContext object for this module (optional).

    Returns:
        Formatted prompt string.
    """
    # Load template
    template_file = files("glreview.templates").joinpath("claude_review_prompt.md")
    template_content = template_file.read_text()

    # autoescape=False is safe: output is markdown for GitLab/Claude, not browser HTML
    env = Environment(loader=BaseLoader(), autoescape=False)  # noqa: S701
    template = env.from_string(template_content)

    # Analyze module
    summary = analyze_module(path, cwd=config.root)
    module_contents = summary.format_summary()

    # Determine if this is a re-review
    is_rereview = (
        module_status.last_reviewed_commit is not None
        and module_status.status in ("in_progress", "reviewed")
    )

    # Build project context string
    project_context_str = ""
    if project_context:
        parts = []
        if project_context.conventions:
            parts.append(f"**Conventions:** {project_context.conventions}")
        if project_context.architecture:
            parts.append(f"**Architecture:** {project_context.architecture}")
        if parts:
            project_context_str = "\n\n".join(parts)

    # Get module purpose from context
    module_purpose = ""
    if module_context:
        module_purpose = module_context.purpose

    # Load test files if available in context
    test_files = []
    if module_context and module_context.test_files:
        for test_path in module_context.test_files:
            test_full_path = config.root / test_path
            if test_full_path.exists():
                try:
                    test_content = test_full_path.read_text()
                    # Truncate very large test files
                    if len(test_content) > 15000:
                        test_content = test_content[:15000] + "\n\n# ... (truncated)"
                    test_files.append({"path": test_path, "content": test_content})
                except (OSError, UnicodeDecodeError):
                    pass

    # Build context
    context = {
        "path": path,
        "lines": module_status.lines,
        "priority": module_status.priority,
        "priority_description": get_priority_description(module_status.priority),
        "module_contents": module_contents,
        "source_code": source_code,
        "is_rereview": is_rereview,
        "previous_commit": module_status.last_reviewed_commit,
        "previous_date": module_status.last_reviewed_date or "unknown",
        "previous_reviewers": ", ".join(module_status.reviewers) or "none",
        "diff": diff,
        "project_context": project_context_str,
        "module_purpose": module_purpose,
        "test_files": test_files,
    }

    prompt = template.render(**context)

    # Add chunk info if applicable
    if chunk_info and chunk_info.total_chunks > 1:
        chunk_header = f"""
**NOTE: This is chunk {chunk_info.chunk_num} of {chunk_info.total_chunks}**
**Lines {chunk_info.start_line}-{chunk_info.end_line}**

Review this portion. Note: you may not have full context from other chunks.

---

"""
        # Insert after the first line
        lines = prompt.split("\n", 1)
        prompt = lines[0] + "\n" + chunk_header + lines[1]

    return prompt


def run_claude_review(
    prompt: str,
    model: str = "sonnet",
) -> tuple[str, bool]:
    """Run Claude CLI with the review prompt.

    Args:
        prompt: The review prompt.
        model: Claude model to use.

    Returns:
        Tuple of (response_text, success).
    """
    if not claude_available():
        return "Error: claude CLI not found. Install Claude Code first.", False

    try:
        # Run claude with --print to get output directly
        result = subprocess.run(
            ["claude", "--print", "--model", model, prompt],
            capture_output=True,
            text=True,
            timeout=300,  # 5 minute timeout
        )

        if result.returncode != 0:
            return f"Error running claude: {result.stderr}", False

        return result.stdout, True

    except subprocess.TimeoutExpired:
        return "Error: Claude review timed out after 5 minutes.", False
    except Exception as e:
        return f"Error: {e}", False


def get_file_diff(
    path: str,
    since_commit: str,
    cwd: Path | None = None,
) -> str | None:
    """Get diff for a file since a given commit.

    Args:
        path: File path.
        since_commit: Commit to diff from.
        cwd: Working directory.

    Returns:
        Diff string, or None if no diff or error.
    """
    try:
        result = subprocess.run(
            ["git", "diff", since_commit, "HEAD", "--", path],
            cwd=cwd,
            capture_output=True,
            text=True,
        )
        if result.returncode == 0 and result.stdout.strip():
            return result.stdout
    except OSError:
        # git not available or other OS error
        return None
    return None
