from __future__ import annotations

import asyncio
import os
from contextlib import asynccontextmanager
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 (stdio)
HTTP_POLL_MAX_ATTEMPTS = 5  # ~60 seconds (HTTP)


def _is_http_transport(ctx: Context | None) -> bool:
    """Check if the server is running over HTTP transport."""
    if ctx is None:
        return False
    try:
        return ctx.request_context.request is not None
    except AttributeError:
        return False


async def submit_review(
    repository: str,
    diff: str = "",
    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".

    Recommended workflow — run these shell commands first, then call this tool:
    1. Resolve the base branch:
           BASE_BRANCH=$(gh pr view --json baseRefName -q '.baseRefName' 2>/dev/null || \
                         git remote show origin | sed -n '/HEAD branch/s/.*: //p')
    2. Compute base_commit (must exist in remote repo history):
           BASE_COMMIT=$(git rev-parse "$BASE_BRANCH")
    3. Derive repository slug from git remote URL:
           REPO=$(git remote get-url origin | sed -E 's#(git@github.com:|https://github.com/)##; s/\\.git$//')
    4. Generate the diff:
           DIFF=$(git diff "$BASE_BRANCH" --no-color)
    5. Call this tool with diff=DIFF, repository=REPO, base_commit=BASE_COMMIT.

    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.
    """
    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:
        async with _get_client(ctx) as client:
            result = await client.create_review(diff, repository, base_commit)
    except (PropelAPIError, ValueError, RuntimeError) 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 = [
        f"Review submitted successfully! 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 += [
        "",
        f"Review {review_id} is now being processed — this typically takes 5-10 minutes.",
        "",
        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.

    If the review is still in progress when this tool returns, call get_review
    again with the same review_id to continue polling.

    After the review completes:
    1. Present all comments to the user grouped by file, with line numbers and severity.
    2. Ask the user if they would like you to fix the flagged comments.
    3. If the user agrees, for each comment:
       a. Determine whether the comment is valid and applicable to the current code.
       b. If valid, apply the fix in the codebase.
       c. Immediately call submit_comment_feedback with incorporated=true and notes
          describing what you changed (e.g. "Added nil check before dereferencing user").
       d. If invalid or not applicable, call submit_comment_feedback with
          incorporated=false and notes explaining why (e.g. "Already handled by middleware").
    4. Do NOT wait for the user to ask about feedback — submit it automatically for
       every comment once you have decided whether to incorporate it or not.
    """
    is_http = _is_http_transport(ctx)
    max_attempts = HTTP_POLL_MAX_ATTEMPTS if is_http else POLL_MAX_ATTEMPTS

    try:
        async with _get_client(ctx) as client:
            status = "unknown"
            for attempt in range(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=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)"
                        )

                result = await client.get_review(review_id)
                status = result.get("status", "unknown")

                if status == REVIEW_STATUS_COMPLETED:
                    formatted = _format_completed_review(result)
                    comments = result.get("comments") or []
                    if comments and ctx:
                        await ctx.info(
                            "Review complete. Next steps:\n"
                            "1. Present the comments above to the user grouped by file.\n"
                            "2. Ask the user: 'Would you like me to fix these comments?'\n"
                            "3. If the user agrees, for EACH comment:\n"
                            "   a. Decide if it is valid and applicable to the current code.\n"
                            "   b. If valid: apply the fix, then call submit_comment_feedback "
                            "with incorporated=true and notes describing the change "
                            "(e.g. 'Added nil check before dereferencing user').\n"
                            "   c. If not applicable: call submit_comment_feedback with "
                            "incorporated=false and notes explaining why "
                            "(e.g. 'Already handled by middleware — no change needed').\n"
                            "4. Use the comment_id shown next to each comment above.\n"
                            "5. Do NOT skip any comment — every comment must receive a feedback call.\n"
                            "6. Do NOT wait for the user to ask about feedback — submit it automatically."
                        )
                    return formatted

                if status == REVIEW_STATUS_FAILED:
                    return _format_failed_review(result)

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

            timeout_seconds = max_attempts * POLL_INTERVAL_SECONDS
            return (
                f"Review {review_id} is still {status} after ~{timeout_seconds} seconds.\n"
                f'Call get_review again with review_id "{review_id}" to continue polling.'
            )
    except asyncio.CancelledError:
        return f"Polling for review {review_id} was interrupted. Use check_review_status to check its current state."
    except (PropelAPIError, ValueError, RuntimeError) as exc:
        return f"Error: {exc}"


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.

    Status values and recommended next actions:
    - "queued"    — review is waiting to be processed; check again later or call get_review to wait.
    - "running"   — review is actively being generated; call get_review to poll until done.
    - "completed" — review finished; call get_review to retrieve and present the comments.
    - "failed"    — review could not be completed; the result includes an error message.
      Consider resubmitting with submit_review if a transient failure is suspected.
    """
    if ctx:
        await ctx.info(f"Checking status of review {review_id}")

    try:
        async with _get_client(ctx) as client:
            result = await client.get_review(review_id)
    except (PropelAPIError, ValueError, RuntimeError) 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)


async def submit_comment_feedback(
    review_id: str,
    incorporated: bool,
    comment_id: str | None = None,
    file_path: str | None = None,
    line: int | None = None,
    message: str | None = None,
    severity: str | None = None,
    notes: str | None = None,
    *,
    ctx: Context | None = None,
) -> str:
    """Submit feedback on a review comment to help Propel improve future reviews.

    Records whether a specific code review comment was incorporated (acted upon)
    by the developer. This feedback is used by Propel to continuously improve
    the quality and relevance of future code reviews.

    IMPORTANT — Automatic usage: When the user asks you to fix or address review
    comments from get_review, you MUST call this tool for each comment after you
    fix it (incorporated=true) or decide it is not applicable (incorporated=false).
    Do NOT wait for the user to ask you to submit feedback — do it automatically
    as part of the fix workflow.

    ALWAYS include the notes parameter with a brief explanation:
    - When incorporated=true: describe what you changed.
      Example notes: "Added nil check before dereferencing user", "Extracted constant MAX_RETRIES=3"
    - When incorporated=false: explain why the comment was dismissed.
      Example notes: "Already handled by middleware — no change needed",
                     "Comment references deleted code — no longer applicable"

    Feedback for all comments in a review should be submitted in a single pass —
    do not submit partial feedback and stop. Every comment returned by get_review
    must receive a feedback call, whether incorporated or not.

    The target comment can be identified in two ways:
    - By comment_id (preferred): Use the comment_id from the get_review response.
    - By comment details: Provide file_path, line, message, and severity.
      The server will deterministically derive the comment_id from these fields.

    If both comment_id and comment details are provided, comment_id takes precedence.

    The review must be in completed status. Submitting feedback against a review
    that is still queued, running, or failed returns an error.

    Common errors:
    - 404: The review_id or comment_id does not exist. Verify you are using the
      exact IDs returned by submit_review and get_review.
    - 409: Feedback already submitted for this comment. This is safe to ignore.
    - 422: The review is not in completed status yet. Wait for get_review to finish.

    Args:
        review_id: The unique identifier of the review (UUID from submit_review).
        incorporated: Whether the developer acted on this comment. True if the
            suggestion was applied or addressed, False if dismissed.
        comment_id: The deterministic identifier of the comment (from get_review).
        file_path: Path of the file the comment applies to (alternative to comment_id).
        line: Line number in the file the comment refers to (alternative to comment_id).
        message: The review comment message body (alternative to comment_id).
        severity: Severity level — one of: critical, high, medium, low (alternative to comment_id).
        notes: Brief explanation of what was changed or why the comment was dismissed.
    """
    # Build the comment object if individual fields are provided
    comment = None
    if comment_id is None:
        if file_path is None or line is None or message is None or severity is None:
            return (
                "Error: Either comment_id or all four comment fields "
                "(file_path, line, message, severity) must be provided."
            )
        if severity not in ("critical", "high", "medium", "low"):
            return f'Error: severity must be one of: critical, high, medium, low (got "{severity}")'
        comment = {
            "file_path": file_path,
            "line": line,
            "message": message,
            "severity": severity,
        }

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

    try:
        async with _get_client(ctx) as client:
            result = await client.submit_comment_feedback(
                review_id=review_id,
                incorporated=incorporated,
                comment_id=comment_id,
                comment=comment,
                notes=notes,
            )
    except (PropelAPIError, ValueError, RuntimeError) as exc:
        return f"Error: {exc}"

    resolved_comment_id = result.get("comment_id", comment_id or "unknown")
    action = "incorporated" if result.get("incorporated", incorporated) else "dismissed"

    lines = [
        "Feedback recorded successfully!",
        "",
        f"Review ID: {result.get('review_id', review_id)}",
        f"Comment ID: {resolved_comment_id}",
        f"Status: {action}",
    ]
    if notes:
        lines.append(f"Notes: {notes}")
    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:
        cid = comment.get("comment_id", "")
        file_path = comment.get("file_path", "unknown")
        line = comment.get("line", 0)
        severity = comment.get("severity", "info")
        message = comment.get("message", "")

        header = f"  {file_path}:{line} [{severity}]"
        if cid:
            header += f" (comment_id: {cid})"
        lines.append(header)
        for msg_line in message.strip().splitlines():
            lines.append(f"    {msg_line}")
        lines.append("")

    lines.append(
        "You can ask me to fix these comments, and feedback will be submitted to Propel automatically."
    )

    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 _extract_token(ctx: Context) -> str:
    """Extract API token from HTTP request headers or fall back to env var."""
    request = ctx.request_context.request
    if request is not None:
        # HTTP mode: ONLY use the Authorization header, never fall back to env var
        auth_header = request.headers.get("authorization", "")
        if auth_header.lower().startswith("bearer "):
            token = auth_header[7:].strip()
            if token:
                return token
        raise ValueError(
            "Missing or invalid Authorization header.\n"
            "Expected: 'Authorization: Bearer <token>'\n"
            "\n"
            "To get a token:\n"
            "  1. Go to https://app.propelcode.ai/administration/settings?tab=review-api-tokens\n"
            "  2. Create a token with 'reviews:write' scope"
        )

    # Stdio mode: use env var
    token = os.environ.get("PROPEL_API_TOKEN", "").strip()
    if token:
        return token

    raise ValueError(
        "No API token provided. Set the PROPEL_API_TOKEN environment variable.\n"
        "\n"
        "To get a token:\n"
        "  1. Go to https://app.propelcode.ai/administration/settings?tab=review-api-tokens\n"
        "  2. Create a token with 'reviews:write' scope"
    )


@asynccontextmanager
async def _get_client(ctx: Context | None):
    """Create a per-request PropelClient and ensure it is closed after use."""
    if ctx is None:
        raise RuntimeError("MCP context is required")

    token = _extract_token(ctx)
    config = ctx.request_context.lifespan_context
    base_url = config.get("base_url", "https://api.propelcode.ai")

    client = PropelClient(api_token=token, base_url=base_url)
    try:
        yield client
    finally:
        await client.aclose()
