"""Review service - orchestrates business logic.

This module provides a high-level API for review operations,
decoupling business logic from CLI presentation.

Usage:
    config = load_config()
    registry = load_registry(config.registry_path)
    service = ReviewService(registry, config)

    # Get status
    state = service.get_registry_state()

    # Start a review
    result = service.start_review("src/module.py", assignee="@reviewer")

    # Sign off
    result = service.signoff("src/module.py", reviewer="@reviewer")
"""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable

from . import state_machine
from .errors import (
    FileChangedDuringReviewError,
    IssueNotClosedError,
    ModuleNotFoundError,
)
from .git import (
    get_current_commit,
    get_gitlab_host,
    get_gitlab_project,
    has_changes_since,
)
from .gitlab import (
    add_issue_comment,
    create_issue,
    gitlab_available,
    update_issue_review_label,
)
from .review_state import RegistryStateInfo, ReviewStateChecker

if TYPE_CHECKING:
    from .claude import ReviewItem
    from .config import Config
    from .registry import ModuleStatus, Registry


@dataclass
class ReviewResult:
    """Result of a review operation."""

    success: bool
    message: str
    module: "ModuleStatus | None" = None
    issue_url: str | None = None
    commit: str | None = None


@dataclass
class ClaudeReviewResult:
    """Result of a Claude review operation."""

    success: bool
    message: str
    review_text: str | None = None
    overall_status: str | None = None  # "OK" or "REQUIRED"
    required_items: list["ReviewItem"] = field(default_factory=list)
    suggested_items: list["ReviewItem"] = field(default_factory=list)
    posted: bool = False
    prompt: str | None = None  # Set when dry_run=True
    chunk_count: int = 1
    has_context: bool = False


@dataclass
class BatchStartResult:
    """Result of a batch start operation."""

    started: int = 0
    reviewed: int = 0
    errors: int = 0
    results: list[tuple[str, ReviewResult]] = field(default_factory=list)


@dataclass
class GitLabInfo:
    """GitLab connection information."""

    host: str
    project: str
    available: bool


def get_gitlab_info(config: "Config") -> GitLabInfo | None:
    """Get GitLab host, project, and availability.

    Args:
        config: Config with project root.

    Returns:
        GitLabInfo if GitLab is configured and available, None otherwise.
    """
    host = get_gitlab_host(cwd=config.root)
    project = get_gitlab_project(cwd=config.root)

    if not host or not project:
        return None

    available = gitlab_available(host)
    return GitLabInfo(host=host, project=project, available=available)


def _load_issue_template(config: "Config") -> str:
    """Load the issue template content.

    Uses custom template from config.issue_template if specified,
    otherwise falls back to the default bundled template.
    """
    from importlib.resources import files

    if config.issue_template:
        custom_path = config.root / config.issue_template
        if custom_path.exists():
            return custom_path.read_text()
        # Fall through to default if custom not found

    template_file = files("glreview.templates").joinpath("issue.md")
    return template_file.read_text()


def _render_issue_description(
    config: "Config",
    path: str,
    module: "ModuleStatus",
    current_commit: str,
    diff_url: str | None = None,
) -> str:
    """Render the issue description from template."""
    from jinja2 import BaseLoader, Environment

    from .analyze import analyze_module

    template_content = _load_issue_template(config)

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

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

    context = {
        "path": path,
        "lines": module.lines,
        "current_commit": current_commit,
        "previous_commit": (
            module.last_reviewed_commit if module.status == "reviewed" else None
        ),
        "previous_date": module.last_reviewed_date or "unknown",
        "previous_reviewers": ", ".join(module.reviewers) or "none",
        "diff_url": diff_url or "",
        "module_contents": module_contents,
        "classes": summary.public_classes,
        "functions": summary.public_functions,
    }

    return template.render(**context)


class ReviewService:
    """High-level API for review operations.

    This class:
    - Orchestrates business logic
    - Provides a clean interface for CLI or other consumers
    - Handles error cases consistently
    - Does NOT handle presentation (that's the CLI's job)
    """

    def __init__(self, registry: "Registry", config: "Config"):
        self.registry = registry
        self.config = config
        self.state_checker = ReviewStateChecker(config)

    def get_module(self, path: str) -> "ModuleStatus":
        """Get a module by path.

        Raises:
            ModuleNotFoundError: If module not in registry.
        """
        module = self.registry.get(path)
        if not module:
            raise ModuleNotFoundError(path)
        return module

    def get_registry_state(self) -> RegistryStateInfo:
        """Get categorized state of all modules."""
        return self.state_checker.get_registry_state(self.registry)

    def get_current_commit(self) -> str:
        """Get current git commit."""
        return get_current_commit(cwd=self.config.root)

    def check_file_changed_during_review(
        self, module: "ModuleStatus"
    ) -> tuple[bool, str | None, str | None]:
        """Check if file changed since review started.

        Returns:
            Tuple of (changed, start_commit, current_commit).
        """
        if not module.review_started_commit:
            return False, None, None

        current = self.get_current_commit()
        changed = has_changes_since(
            module.path,
            module.review_started_commit,
            cwd=self.config.root,
        )
        return changed, module.review_started_commit, current

    def start_review(
        self,
        path: str,
        assignee: str | None = None,
        issue_url: str | None = None,
        force: bool = False,
        create_issue: bool = True,
    ) -> ReviewResult:
        """Start a review for a module.

        Args:
            path: Module path.
            assignee: Reviewer to assign.
            issue_url: Pre-existing issue URL (if not creating new).
            force: Force start even if already reviewed.
            create_issue: Whether to create a GitLab issue.

        Returns:
            ReviewResult with success/failure info.
        """
        try:
            module = self.get_module(path)
        except ModuleNotFoundError as e:
            return ReviewResult(False, str(e))

        # Check if already in progress
        if module.status == "in_progress" and not force:
            return ReviewResult(
                False,
                f"Review already in progress for {path}",
                module=module,
                issue_url=module.review_issue,
            )

        # Check if already reviewed and current
        if module.status == "reviewed" and not force:
            is_stale, _ = self.state_checker.is_module_stale(module)
            if not is_stale:
                return ReviewResult(
                    False,
                    f"{path} is already reviewed at current commit",
                    module=module,
                )

        current_commit = self.get_current_commit()

        # Create GitLab issue if requested and no URL provided
        if create_issue and not issue_url:
            created_issue = self._create_review_issue(
                path, module, current_commit, assignee
            )
            if created_issue:
                issue_url = created_issue
            else:
                # Issue creation was requested but failed
                return ReviewResult(
                    False,
                    "Failed to create GitLab issue",
                    module=module,
                )

        try:
            state_machine.start_review(
                module,
                current_commit,
                issue_url=issue_url,
                assignee=assignee,
                force=force,
            )
        except Exception as e:
            return ReviewResult(False, str(e), module=module)

        return ReviewResult(
            True,
            f"Review started for {path}",
            module=module,
            issue_url=issue_url,
            commit=current_commit,
        )

    def _create_review_issue(
        self,
        path: str,
        module: "ModuleStatus",
        current_commit: str,
        assignee: str | None = None,
    ) -> str | None:
        """Create a GitLab issue for the review.

        Returns issue URL on success, None on failure.
        """
        from .gitlab import get_compare_url

        host = get_gitlab_host(cwd=self.config.root)
        project = get_gitlab_project(cwd=self.config.root)

        if not gitlab_available(host) or not host:
            return None

        # Build diff URL for re-reviews
        diff_url = None
        if module.last_reviewed_commit and module.status == "reviewed":
            diff_url = get_compare_url(
                project, host, module.last_reviewed_commit, current_commit
            )

        description = _render_issue_description(
            config=self.config,
            path=path,
            module=module,
            current_commit=current_commit,
            diff_url=diff_url,
        )

        labels = self.config.issue_labels
        module_name = Path(path).stem

        created = create_issue(
            title=f"[REVIEW] {module_name}",
            description=description,
            project_path=project,
            host=host,
            labels=labels,
            assignee=assignee,
        )

        if created:
            return created.url or f"#{created.number}"
        return None

    def signoff(
        self,
        path: str,
        reviewer: str,
        force: bool = False,
        acknowledge_changes: bool = False,
    ) -> ReviewResult:
        """Sign off on a completed review.

        Args:
            path: Module path.
            reviewer: Reviewer username(s).
            force: Force signoff regardless of state.
            acknowledge_changes: Acknowledge that file changed during review.

        Returns:
            ReviewResult with success/failure info.
        """
        try:
            module = self.get_module(path)
        except ModuleNotFoundError as e:
            return ReviewResult(False, str(e))

        # Check for changes during review
        if not acknowledge_changes and not force:
            changed, start_commit, current_commit = (
                self.check_file_changed_during_review(module)
            )
            if changed and start_commit and current_commit:
                raise FileChangedDuringReviewError(path, start_commit, current_commit)

        current_commit = self.get_current_commit()

        try:
            state_machine.signoff(
                module,
                current_commit,
                reviewer,
                force=force,
            )
        except Exception as e:
            return ReviewResult(False, str(e), module=module)

        return ReviewResult(
            True,
            f"Signed off on {path}",
            module=module,
            commit=current_commit,
        )

    def cancel_review(
        self,
        path: str,
        force: bool = False,
    ) -> ReviewResult:
        """Cancel a review.

        Args:
            path: Module path.
            force: Force cancel regardless of state.

        Returns:
            ReviewResult with the new status.
        """
        try:
            module = self.get_module(path)
        except ModuleNotFoundError as e:
            return ReviewResult(False, str(e))

        try:
            new_status = state_machine.cancel_review(module, force=force)
        except Exception as e:
            return ReviewResult(False, str(e), module=module)

        return ReviewResult(
            True,
            f"Review canceled for {path}. Status: {new_status}",
            module=module,
        )

    def restart_review(
        self,
        path: str,
    ) -> ReviewResult:
        """Restart a review at current commit.

        Args:
            path: Module path.

        Returns:
            ReviewResult with the new start commit.
        """
        try:
            module = self.get_module(path)
        except ModuleNotFoundError as e:
            return ReviewResult(False, str(e))

        current_commit = self.get_current_commit()

        try:
            state_machine.restart_review(module, current_commit)
        except Exception as e:
            return ReviewResult(False, str(e), module=module)

        return ReviewResult(
            True,
            f"Review restarted for {path} at {current_commit}",
            module=module,
            commit=current_commit,
        )

    def verify_issue_closed(
        self,
        module: "ModuleStatus",
        skip_verify: bool = False,
    ) -> tuple[bool, str | None, list[str]]:
        """Verify that a module's review issue is closed.

        Args:
            module: The module to check.
            skip_verify: If True, skip the check.

        Returns:
            Tuple of (is_closed, error_message, assignees).
            assignees is populated if issue was fetched successfully.
        """
        if skip_verify:
            return True, None, []

        issue_number = ReviewStateChecker.extract_issue_number(module.review_issue)
        if not issue_number:
            return True, None, []  # No issue to verify

        from .git import get_gitlab_host, get_gitlab_project
        from .gitlab import get_issue, gitlab_available

        host = get_gitlab_host(cwd=self.config.root)
        project = get_gitlab_project(cwd=self.config.root)

        if not gitlab_available(host):
            return True, "GitLab not available (no token)", []

        issue = get_issue(issue_number, project_path=project, host=host)
        if not issue:
            return True, f"Could not fetch issue #{issue_number}", []

        if issue.state != "closed":
            raise IssueNotClosedError(issue_number, issue.state)

        return True, None, issue.assignees

    def get_summary(self) -> dict:
        """Get review coverage summary.

        Returns dict with:
        - total, reviewed, in_progress, needs_review counts
        - total_lines, reviewed_lines
        - coverage_pct, line_coverage_pct
        - stale_count (reviewed but changed)
        """
        state = self.get_registry_state()

        total = state.total
        reviewed = state.reviewed_count
        in_progress = len(state.in_progress)
        needs_review = len(state.needs_review)
        stale = len(state.reviewed_stale)

        total_lines = state.total_lines
        reviewed_lines = state.reviewed_lines

        coverage_pct = (reviewed / total * 100) if total > 0 else 0
        line_coverage_pct = (
            (reviewed_lines / total_lines * 100) if total_lines > 0 else 0
        )

        return {
            "total": total,
            "reviewed": reviewed,
            "in_progress": in_progress,
            "needs_review": needs_review,
            "stale": stale,
            "total_lines": total_lines,
            "reviewed_lines": reviewed_lines,
            "coverage_pct": round(coverage_pct, 1),
            "line_coverage_pct": round(line_coverage_pct, 1),
        }

    # -------------------------------------------------------------------------
    # Claude review operations
    # -------------------------------------------------------------------------

    def run_claude_review(
        self,
        path: str,
        model: str = "opus",
        post: bool = False,
        no_context: bool = False,
        diff_only: bool = False,
        dry_run: bool = False,
        on_progress: Callable[[str, dict[str, Any]], None] | None = None,
    ) -> ClaudeReviewResult:
        """Run Claude AI-assisted review on a module.

        Args:
            path: Module path.
            model: Claude model to use.
            post: Whether to post findings to GitLab issue.
            no_context: Skip loading project context.
            diff_only: Only include diff in review (for re-reviews).
            dry_run: If True, return the prompt without running Claude.
            on_progress: Optional callback(event, data) for progress updates.
                Events: 'context_loaded', 'chunk_start', 'chunk_done', 'large_file'

        Returns:
            ClaudeReviewResult with review findings.
        """
        from .claude import (
            build_review_prompt,
            chunk_source_code,
            get_file_diff,
            parse_review_output,
            run_claude_review,
        )
        from .claude_context import get_default_context_path, load_context

        try:
            module = self.get_module(path)
        except ModuleNotFoundError as e:
            return ClaudeReviewResult(False, str(e))

        if module.status != "in_progress":
            return ClaudeReviewResult(
                False,
                f"{path} is not in progress (status: {module.status})",
            )

        # Load project context if available
        project_ctx = None
        module_ctx = None
        has_context = False
        if not no_context:
            context_path = get_default_context_path(self.config.root)
            if context_path.exists():
                project_ctx = load_context(context_path)
                module_ctx = project_ctx.get_module(path)
                if module_ctx:
                    has_context = True
                    if on_progress:
                        on_progress(
                            "context_loaded",
                            {
                                "path": path,
                                "test_files": len(module_ctx.test_files),
                            },
                        )

        # Read source file
        source_path = self.config.root / path
        if not source_path.exists():
            return ClaudeReviewResult(False, f"File not found: {source_path}")

        source_code = source_path.read_text()

        # Get diff if re-review
        diff = None
        if module.last_reviewed_commit:
            diff = get_file_diff(
                path, module.last_reviewed_commit, cwd=self.config.root
            )

        # Process chunks
        chunks = chunk_source_code(source_code)

        if len(chunks) > 1 and on_progress:
            on_progress("large_file", {"chunk_count": len(chunks)})

        # For dry-run, just return the first prompt
        if dry_run:
            chunk = chunks[0]
            code_to_review = chunk.content
            if diff_only and diff:
                code_to_review = f"# Diff only mode - showing changes\n\n{diff}"

            prompt = build_review_prompt(
                path=path,
                source_code=code_to_review,
                module_status=module,
                config=self.config,
                diff=diff if not diff_only else None,
                chunk_info=chunk if len(chunks) > 1 else None,
                project_context=project_ctx,
                module_context=module_ctx,
            )
            return ClaudeReviewResult(
                success=True,
                message="Dry run - prompt generated",
                prompt=prompt,
                chunk_count=len(chunks),
                has_context=has_context,
            )

        all_results = []

        for chunk in chunks:
            if on_progress and len(chunks) > 1:
                on_progress(
                    "chunk_start",
                    {
                        "chunk_num": chunk.chunk_num,
                        "total_chunks": chunk.total_chunks,
                        "start_line": chunk.start_line,
                        "end_line": chunk.end_line,
                    },
                )

            code_to_review = chunk.content
            if diff_only and diff:
                code_to_review = f"# Diff only mode - showing changes\n\n{diff}"

            prompt = build_review_prompt(
                path=path,
                source_code=code_to_review,
                module_status=module,
                config=self.config,
                diff=diff if not diff_only else None,
                chunk_info=chunk if len(chunks) > 1 else None,
                project_context=project_ctx,
                module_context=module_ctx,
            )

            result, success = run_claude_review(prompt, model=model)
            if not success:
                return ClaudeReviewResult(False, result)

            all_results.append(result)

            if on_progress and len(chunks) > 1:
                on_progress("chunk_done", {"chunk_num": chunk.chunk_num})

        # Combine results
        if len(all_results) == 1:
            final_result = all_results[0]
        else:
            final_result = "# Combined Review (Multiple Chunks)\n\n"
            for i, result in enumerate(all_results, 1):
                final_result += f"## Chunk {i}\n\n{result}\n\n---\n\n"

        # Parse review
        parsed = parse_review_output(final_result)

        # Post to issue if requested
        posted = False
        if post:
            posted = self._post_review_to_issue(module, final_result, parsed)

        return ClaudeReviewResult(
            success=True,
            message="Review completed",
            review_text=final_result,
            overall_status=parsed.overall_assessment,
            required_items=parsed.required_items,
            suggested_items=parsed.suggested_items,
            posted=posted,
            chunk_count=len(chunks),
            has_context=has_context,
        )

    def _post_review_to_issue(
        self, module: "ModuleStatus", review_text: str, parsed
    ) -> bool:
        """Post review findings to GitLab issue.

        Returns True if posted successfully.
        """
        try:
            issue_number = ReviewStateChecker.extract_issue_number(module.review_issue)
            if not issue_number:
                return False

            project = get_gitlab_project(cwd=self.config.root)
            host = get_gitlab_host(cwd=self.config.root)

            if not gitlab_available(host):
                return False

            comment = f"## Claude Code Review\n\n{review_text}\n\n"
            comment += "---\n_Generated by `glreview claude-review`_"

            if not add_issue_comment(issue_number, project, comment, host=host):
                return False

            # Update label based on review
            new_label = "required" if parsed.required_items else "ok"
            update_issue_review_label(issue_number, project, new_label, host)

            return True
        except Exception:
            # Connection errors or other transient failures
            return False

    # -------------------------------------------------------------------------
    # Batch operations
    # -------------------------------------------------------------------------

    def get_modules_needing_review(self) -> list[tuple[str, "ModuleStatus"]]:
        """Get all modules with status 'needs_review'."""
        return [
            (path, mod)
            for path, mod in self.registry.modules.items()
            if mod.status == "needs_review"
        ]

    def batch_start_reviews(
        self,
        claude_review: bool = False,
        model: str = "opus",
        on_progress: Callable[[str, str, Any], None] | None = None,
    ) -> BatchStartResult:
        """Start reviews for all modules needing review.

        Args:
            claude_review: Whether to also run Claude review for each.
            model: Claude model to use (if claude_review=True).
            on_progress: Optional callback(path, step, result) for progress updates.

        Returns:
            BatchStartResult with counts and per-module results.
        """
        needs_review = self.get_modules_needing_review()
        result = BatchStartResult()

        for path, _module in needs_review:
            # Start the review
            start_result = self.start_review(path)
            result.results.append((path, start_result))

            if on_progress:
                on_progress(path, "start", start_result)

            if start_result.success:
                result.started += 1

                # Run Claude review if requested
                if claude_review:
                    review_result = self.run_claude_review(path, model=model, post=True)

                    if on_progress:
                        on_progress(path, "review", review_result)

                    if review_result.success and review_result.posted:
                        result.reviewed += 1
            else:
                result.errors += 1

        return result
