from __future__ import annotations

import asyncio
import pathlib
from typing import Any

from mcp.server.fastmcp import Context

from mcp_server_propel.propel_client import (
    MAX_DIFF_BYTES,
    REVIEW_STATUS_COMPLETED,
    REVIEW_STATUS_FAILED,
    REVIEW_STATUS_QUEUED,
    REVIEW_STATUS_RUNNING,
    PropelAPIError,
    PropelClient,
)

POLL_INTERVAL_SECONDS = 12
POLL_MAX_ATTEMPTS = 60  # ~12 minutes


async def submit_review(
    repository: str,
    diff: str = "",
    diff_path: str | None = None,
    base_commit: str | None = None,
    *,
    ctx: Context | None = None,
) -> str:
    """Submit a unified git diff for AI-powered code review on Propel.

    When to use: The user wants to review code changes, get feedback on a PR, or
    have their diff analyzed. Trigger phrases: "review my changes", "check my code",
    "review this PR", "what do you think of this diff".

    How to prepare the diff:
    1. Run `git diff <base>..HEAD --no-color > /tmp/propel_diff.txt` in the repo.
    2. Pass diff_path="/tmp/propel_diff.txt" (preferred for large diffs).
    3. Or pass the diff string directly in the diff parameter for small diffs.

    IMPORTANT: The diff MUST be a complete unified git diff (full output of
    `git diff`). Do NOT truncate, edit, or paste partial diffs — the backend
    parser will reject them. Maximum size is 1 MB.

    How to get repository: Run `git remote get-url origin`, extract "owner/repo"
    from the URL (e.g. "git@github.com:myorg/myrepo.git" → "myorg/myrepo").
    The repository must be configured in the user's Propel account.

    How to get base_commit: Run `git merge-base main HEAD` (or whatever the
    default branch is). This is optional but improves review quality.

    After this tool returns, you MUST immediately call get_review with the
    returned review_id to poll for results. Do not wait for the user to ask.
    """
    client = _get_client(ctx)

    if diff_path:
        path = pathlib.Path(diff_path).expanduser()
        if not path.is_file():
            return f"Error: diff_path is not a file: {diff_path}"

        try:
            file_size = path.stat().st_size
        except OSError as exc:
            return f"Error: could not read diff_path: {exc}"

        if file_size > MAX_DIFF_BYTES:
            return (
                f"Error: File too large ({file_size:,} bytes). "
                f"Maximum size is 1 MB ({MAX_DIFF_BYTES:,} bytes). "
                "Try reviewing a smaller changeset."
            )

        try:
            diff = await asyncio.to_thread(path.read_text, encoding="utf-8")
        except UnicodeDecodeError:
            return f"Error: diff_path is not a text file: {diff_path}"
        except OSError as exc:
            return f"Error: could not read diff_path: {exc}"

    diff_bytes = len(diff.encode("utf-8"))
    if diff_bytes > MAX_DIFF_BYTES:
        return (
            f"Error: Diff too large ({diff_bytes:,} bytes). "
            f"Maximum size is 1 MB ({MAX_DIFF_BYTES:,} bytes). "
            "Try reviewing a smaller changeset."
        )

    if ctx:
        await ctx.info(f"Submitting review for repository: {repository}")

    try:
        result = await client.create_review(diff, repository, base_commit)
    except PropelAPIError as exc:
        return f"Error: {exc}"

    review_id = result.get("review_id", "unknown")
    status = result.get("status", "unknown")
    repo = result.get("repository", repository)
    created_at = result.get("created_at", "")
    base = result.get("base_commit", "")

    lines = [
        "Review submitted successfully!",
        "",
        f"Review ID: {review_id}",
        f"Status: {status}",
        f"Repository: {repo}",
    ]
    if base:
        lines.append(f"Base commit: {base}")
    if created_at:
        lines.append(f"Created: {created_at}")
    lines += [
        "",
        "The review is now being processed and typically takes 5-10 minutes.",
        "Propel will now poll for results — please wait while the review completes.",
        "",
        f'Next step: call get_review with review_id "{review_id}" to poll for results.',
    ]
    return "\n".join(lines)


async def get_review(
    review_id: str,
    *,
    ctx: Context | None = None,
) -> str:
    """Poll for and return the full results of a code review.

    Call this immediately after submit_review returns a review_id. It polls every
    12 seconds for up to ~12 minutes until the review completes or fails.

    This tool is long-running — do NOT abort or cancel the call while it is polling.
    The review typically takes 5-10 minutes to complete.

    Only use this when you have a review_id from submit_review or from the user.
    For a quick non-blocking status check, use check_review_status instead.
    """
    client = _get_client(ctx)

    status = "unknown"
    try:
        for attempt in range(POLL_MAX_ATTEMPTS):
            elapsed_min = (attempt * POLL_INTERVAL_SECONDS) // 60
            elapsed_sec = (attempt * POLL_INTERVAL_SECONDS) % 60

            if ctx:
                await ctx.report_progress(progress=attempt, total=POLL_MAX_ATTEMPTS)
                if elapsed_min > 0:
                    await ctx.info(
                        f"Waiting for review — {elapsed_min}m {elapsed_sec}s elapsed (typically 5-10 min)"
                    )
                else:
                    await ctx.info(
                        f"Waiting for review — {elapsed_sec}s elapsed (typically 5-10 min)"
                    )

            try:
                result = await client.get_review(review_id)
            except PropelAPIError as exc:
                return f"Error: {exc}"

            status = result.get("status", "unknown")

            if status == REVIEW_STATUS_COMPLETED:
                return _format_completed_review(result)

            if status == REVIEW_STATUS_FAILED:
                return _format_failed_review(result)

            if attempt < POLL_MAX_ATTEMPTS - 1:
                await asyncio.sleep(POLL_INTERVAL_SECONDS)

        return (
            f"Review {review_id} is still {status} after ~{POLL_MAX_ATTEMPTS * POLL_INTERVAL_SECONDS // 60} minutes.\n"
            f"You can try again later with get_review or use check_review_status for a quick status check."
        )
    except asyncio.CancelledError:
        return f"Polling for review {review_id} was interrupted. Use check_review_status to check its current state."


async def check_review_status(
    review_id: str,
    *,
    ctx: Context | None = None,
) -> str:
    """Quick status check for a code review without waiting for completion.

    Use this when the user asks "what's the status of my review" or "is my review
    done yet". Returns only the current status and timing info — no comments.

    Unlike get_review, this does NOT poll or wait. It returns immediately with a
    single status check. If the review is complete, suggest calling get_review to
    see the full comments.
    """
    client = _get_client(ctx)

    if ctx:
        await ctx.info(f"Checking status of review {review_id}")

    try:
        result = await client.get_review(review_id)
    except PropelAPIError as exc:
        return f"Error: {exc}"

    status = result.get("status", "unknown")
    lines = [
        f"Review: {review_id}",
        f"Status: {status}",
    ]

    status_fields = [
        ("repository", "Repository"),
        ("base_commit", "Base commit"),
        ("created_at", "Created"),
        ("updated_at", "Updated"),
        ("started_at", "Started"),
        ("completed_at", "Completed"),
    ]
    for key, label in status_fields:
        if result.get(key):
            lines.append(f"{label}: {result[key]}")

    if status in (REVIEW_STATUS_QUEUED, REVIEW_STATUS_RUNNING):
        lines += [
            "",
            "Use get_review to poll and wait for results, or check again later with check_review_status.",
        ]

    if status == REVIEW_STATUS_COMPLETED:
        comment_count = len(result.get("comments") or [])
        lines.append(f"Comments: {comment_count}")
        lines += ["", "Use get_review to see the full review comments."]

    if status == REVIEW_STATUS_FAILED:
        error = result.get("error")
        if error:
            lines.append(f"Error: {error.get('message', 'Unknown error')}")

    return "\n".join(lines)


def _format_completed_review(result: dict[str, Any]) -> str:
    review_id = result.get("review_id", "unknown")
    repo = result.get("repository", "")
    base_commit = result.get("base_commit", "")
    completed_at = result.get("completed_at", "")
    updated_at = result.get("updated_at", "")
    comments = result.get("comments") or []

    lines = [
        f"Review completed for {review_id}",
        "",
    ]
    if repo:
        lines.append(f"Repository: {repo}")
    if base_commit:
        lines.append(f"Base commit: {base_commit}")
    if completed_at:
        lines.append(f"Completed: {completed_at}")
    if updated_at:
        lines.append(f"Updated: {updated_at}")

    if not comments:
        lines += ["", "No issues found — your code looks good!"]
        return "\n".join(lines)

    lines.append(f"\nFound {len(comments)} comment(s):\n")

    for comment in comments:
        file_path = comment.get("file_path", "unknown")
        line = comment.get("line", 0)
        severity = comment.get("severity", "info")
        message = comment.get("message", "")

        lines.append(f"  {file_path}:{line} [{severity}]")
        for msg_line in message.strip().splitlines():
            lines.append(f"    {msg_line}")
        lines.append("")

    return "\n".join(lines)


def _format_failed_review(result: dict[str, Any]) -> str:
    review_id = result.get("review_id", "unknown")
    error = result.get("error")

    lines = [
        f"Review failed for {review_id}",
        "",
    ]

    if error:
        code = error.get("code", "unknown")
        message = error.get("message", "Unknown error")
        lines.append(f"Error code: {code}")
        lines.append(f"Error: {message}")
    else:
        lines.append("No error details available.")

    lines += [
        "",
        "You may want to resubmit the review with submit_review.",
    ]
    return "\n".join(lines)


def _get_client(ctx: Context | None) -> PropelClient:
    if ctx is None:
        raise RuntimeError("MCP context is required")
    client = ctx.request_context.lifespan_context
    if not isinstance(client, PropelClient):
        raise RuntimeError("PropelClient not found in lifespan context")
    return client
