"""MCP server for the review system (stdio + background HTTP API)."""

from __future__ import annotations

import asyncio
import json
import os
import shutil
import subprocess
import threading
from datetime import datetime, timezone
from typing import Optional

from mcp.server.fastmcp import FastMCP

from . import db
from .file_finder import find_source_file

PROJECT_ROOT = os.environ.get("PROJECT_ROOT", "")

mcp = FastMCP("ui-ticket-mcp")


def _format_element_context(metadata_str: str | None) -> str:
    """Format annotation metadata into a human-readable element context line."""
    if not metadata_str:
        return ""
    try:
        meta = json.loads(metadata_str) if isinstance(metadata_str, str) else metadata_str
    except (json.JSONDecodeError, TypeError):
        return ""
    parts = []
    if meta.get("element"):
        parts.append(f"Element: {meta['element']}")
    if meta.get("selector"):
        parts.append(f"Selector: {meta['selector']}")
    if meta.get("selectedText"):
        parts.append(f'Selected: "{meta["selectedText"]}"')
    return " | ".join(parts)


@mcp.tool()
async def get_review_summary() -> str:
    """Get a summary of all pages with open/resolved review counts."""
    await db.init_db()
    rows = await db.get_summary()
    if not rows:
        return "No reviews found."

    lines = ["Page | Open | Resolved | Total", "--- | --- | --- | ---"]
    for r in rows:
        lines.append(f"{r['page_id']} | {r['open']} | {r['resolved']} | {r['total']}")
    return "\n".join(lines)


@mcp.tool()
async def get_reviews(page_id: Optional[str] = None) -> str:
    """Get review comments, optionally filtered by page_id."""
    await db.init_db()
    rows = await db.get_reviews(page_id)
    if not rows:
        return f"No reviews found{' for ' + page_id if page_id else ''}."

    lines = []
    for r in rows:
        status = "OPEN" if r["status"] == "open" else "RESOLVED"
        tag = r.get("tag", "general") or "general"
        parent = f" (reply to #{r['parent_id']})" if r.get("parent_id") else ""
        line = f"[#{r['id']}] [{status}] [{tag}] {r['page_id']} - {r['author']}: {r['text']}{parent}"
        ctx = _format_element_context(r.get("metadata"))
        if ctx:
            line += f"\n    [{ctx}]"
        lines.append(line)
    return "\n".join(lines)


@mcp.tool()
async def add_review(
    page_id: str,
    author: str,
    text: str,
    metadata: Optional[str] = None,
    tag: str = "general",
    parent_id: Optional[int] = None,
) -> str:
    """Add a new review comment to a page. Optional metadata is a JSON string with element annotation context. Tag can be: bug, suggestion, question, general. Set parent_id to reply to an existing review."""
    await db.init_db()
    review = await db.insert_review(page_id, author, text, metadata, tag=tag, parent_id=parent_id)
    return f"Created review #{review['id']} on {page_id} by {author} [{tag}]."


@mcp.tool()
async def resolve_review(review_id: int, resolved_by: str = "agent") -> str:
    """Mark a review as resolved. Only resolve after verifying your changes are scoped correctly — page-specific reviews should not cause global side effects on other pages."""
    await db.init_db()
    now = datetime.now(timezone.utc).isoformat()
    result = await db.update_review(
        review_id, status="resolved", resolved_at=now, resolved_by=resolved_by
    )
    if result is None:
        return f"Review #{review_id} not found."
    return f"Review #{review_id} resolved by {resolved_by}."


@mcp.tool()
async def reopen_review(review_id: int) -> str:
    """Reopen a previously resolved review."""
    await db.init_db()
    result = await db.update_review(
        review_id, status="open", resolved_at=None, resolved_by=None
    )
    if result is None:
        return f"Review #{review_id} not found."
    return f"Review #{review_id} reopened."


@mcp.tool()
async def batch_resolve(page_id: str, resolved_by: str = "agent") -> str:
    """Resolve all open reviews on a page. Use only after all reviews on the page have been addressed. Verify changes are scoped to this page and don't affect other pages unintentionally."""
    await db.init_db()
    count = await db.batch_resolve(page_id, resolved_by)
    return f"Resolved {count} review(s) on {page_id}."


@mcp.tool()
async def get_pending_work() -> str:
    """Get all open reviews grouped by page. Ready-to-work list for agents. Each review is scoped to a page_id — your fixes should target that specific page only. If a review is ambiguous, ask the user for clarification before making changes."""
    await db.init_db()
    pages = await db.get_open_reviews_by_page()
    if not pages:
        return "No pending reviews. All clear!"

    lines = []
    for p in pages:
        lines.append(f"\n## {p['page_id']} ({len(p['reviews'])} open)")
        for r in p["reviews"]:
            tag = r.get("tag", "general") or "general"
            line = f"  - #{r['id']} [{tag}] ({r['author']}): {r['text']}"
            ctx = _format_element_context(r.get("metadata"))
            if ctx:
                line += f"\n    [{ctx}]"
            lines.append(line)
    return "\n".join(lines)


@mcp.tool()
async def find_source_file_tool(page_id: str) -> str:
    """Find source files in PROJECT_ROOT that match a page_id. Use this to locate the page-specific source file before making changes. If multiple files match, prefer the page-specific file over shared/common components. Only modify shared components if the user explicitly requests a global change."""
    if not PROJECT_ROOT:
        return "PROJECT_ROOT environment variable is not set."

    matches = find_source_file(page_id, PROJECT_ROOT)
    if not matches:
        return f"No source files found for page_id '{page_id}' in {PROJECT_ROOT}."

    lines = [f"Found {len(matches)} file(s) for '{page_id}':"]
    for m in matches:
        lines.append(f"  - {m}")
    return "\n".join(lines)


@mcp.tool()
async def get_setup_guide() -> str:
    """Get the full setup guide for the review system (MCP server + REST API + browser UI)."""
    port = os.environ.get("REVIEW_PORT", "3200")
    return f"""# ui-ticket-mcp Setup Guide

MCP server (stdio) + REST API (HTTP on port {port}) start together automatically.

IMPORTANT: The API always runs LOCALLY on localhost:{port}. The website https://uiticket.0ics.ai/ is the landing page only — NOT an API endpoint. Always use http://localhost:{port}/api as the api-url.

## 1. Add to `.mcp.json`

```json
{{
  "mcpServers": {{
    "ui-ticket-mcp": {{
      "command": "uvx",
      "args": ["ui-ticket-mcp"],
      "env": {{
        "PROJECT_ROOT": "/path/to/your/project"
      }}
    }}
  }}
}}
```

Restart the agent. MCP connects via stdio, REST API starts in background on http://localhost:{port}.

Port configurable via `REVIEW_PORT` env var.

## 2. Browser UI (any framework)

### Option A: With a bundler (Angular, React, Vue, Svelte, etc.)

```bash
npm install ui-ticket-panel
```

In your app's entry file (e.g. `main.ts`, `index.tsx`):

```ts
import {{ defineReviewPanel }} from 'ui-ticket-panel';
defineReviewPanel();
```

Then in your root template:

```html
<review-panel api-url="http://localhost:{port}/api"></review-panel>
```

### Option B: Plain HTML / CDN (no bundler needed)

```html
<script type="module" src="https://unpkg.com/ui-ticket-panel/dist/bundle.js"></script>
<review-panel api-url="http://localhost:{port}/api"></review-panel>
```

The bundle auto-registers `<review-panel>`. No npm install, no build step.

### Option C: SSR frameworks (Next.js, Nuxt, SvelteKit)

Web Components use `window` and `HTMLElement` which don't exist during server-side rendering. You MUST load the panel dynamically on the client side only.

**Next.js (app router):**

```tsx
// components/ReviewPanel.tsx
'use client';
import {{ useEffect }} from 'react';

export default function ReviewPanel() {{
  useEffect(() => {{
    import('ui-ticket-panel').then(m => m.defineReviewPanel());
  }}, []);
  return <review-panel api-url="http://localhost:{port}/api" />;
}}
```

**Nuxt:** Use `<ClientOnly>` wrapper + dynamic `import()` in `onMounted()`.

**SvelteKit:** Use dynamic `import()` in `onMount()`.

### Where to place `<review-panel>`

Add it **once** in your app's root layout/shell. One instance covers the entire app.

- **Angular**: `app.component.html` — also add `CUSTOM_ELEMENTS_SCHEMA` to your root module/component
- **React**: `App.tsx` or root layout
- **Vue**: `App.vue`
- **Svelte**: `+layout.svelte`
- **Next.js**: `app/layout.tsx` (using the client component from Option C)
- **HTML**: `index.html`

### Page identification

The panel **automatically detects** the current page from the URL pathname (`/analytics` → `analytics`, `/` → `home`). It also listens for SPA navigation (pushState/replaceState/popstate) and reloads reviews when the route changes. This is the recommended approach — no configuration needed.

Alternatively, you can set `page-id` explicitly:

```html
<review-panel api-url="http://localhost:{port}/api" page-id="dashboard"></review-panel>
```

**Important:** These two modes are mutually exclusive. If you set `page-id`, auto-detection is disabled and the panel will NOT react to route changes. Do not combine both.

## Database

Reviews are stored in `{{PROJECT_ROOT}}/.reviews/reviews.db` (auto-created, committable to git).
Override with `REVIEW_DB_PATH` env var.

## Agent workflow

1. `get_pending_work()` - see what needs attention
2. `find_source_file_tool("page-id")` - locate source files
3. (read/edit the code)
4. `resolve_review(id)` or `batch_resolve("page-id")` - mark as done

## Agent behavior rules

**Ask when uncertain.** If the review text is ambiguous or you're unsure what change is expected, ASK the user for clarification. Never guess or make assumptions — wrong changes are worse than no changes.

**Scope changes to the reviewed page.** Each review is tied to a `page_id`. When fixing a review, your changes MUST be scoped to that specific page only. If the component is reused across multiple pages, apply the fix only where the review was filed (e.g. override styles, use conditional logic, or create a page-specific variant). Do NOT modify the shared/base component in a way that affects all pages.

**Global changes require explicit permission.** Only make a global change (modifying a shared component for all consumers) if the user explicitly asks for it. A review on one page is NOT permission to change behavior on all pages.

**Use element context to locate code.** Call `get_annotated_reviews()` to see exactly which element the reviewer pointed at. Each annotation includes:
- **Element**: human name (e.g. "Button 'Save'", "Table cell 'Scaduta'")
- **Selector**: CSS path to the element (may be ambiguous for repeated structures)
- **Fingerprint**: compact identity — `tag`, `col` (table column index), text content, ARIA role/label. Use this to disambiguate when multiple elements share the same selector
- **Context**: source location as `component > section > landmark` — use this to find the right file/component in code (e.g. "app-posizioni-debitorie > dx-data-grid > main")
- **A11y**: accessibility attributes (role, aria-label, tabindex)
- **Styles**: current computed styles — useful for styling-related reviews

Start with **Context** to find the source file, then use **Element** + **Fingerprint** to locate the exact element in the template.

**Verify scope before resolving.** Before marking a review as resolved, verify your changes only affect the target page. If you changed a shared file, confirm with the user that a global change was intended.
"""


@mcp.tool()
async def get_annotated_reviews(page_id: Optional[str] = None) -> str:
    """Get only reviews that have element annotation metadata. Shows full element context for targeted fixes. Use this to understand exactly which element the reviewer pointed at — includes selector, fingerprint (tag, column, text, role), source context (component > section), accessibility info, and styles. This context helps you make precise, scoped changes."""
    await db.init_db()
    rows = await db.get_reviews(page_id)
    annotated = [r for r in rows if r.get("metadata")]
    if not annotated:
        return f"No annotated reviews found{' for ' + page_id if page_id else ''}."

    lines = []
    for r in annotated:
        status = "OPEN" if r["status"] == "open" else "RESOLVED"
        lines.append(f"[#{r['id']}] [{status}] {r['page_id']} - {r['author']}: {r['text']}")
        try:
            meta = json.loads(r["metadata"]) if isinstance(r["metadata"], str) else r["metadata"]
            if meta.get("element"):
                lines.append(f"    Element: {meta['element']}")
            if meta.get("selector"):
                lines.append(f"    Selector: {meta['selector']}")
            if meta.get("fingerprint"):
                fp = meta["fingerprint"]
                fp_parts = [f"tag={fp.get('tag','')}"]
                if fp.get("colIndex") is not None:
                    fp_parts.append(f"col={fp['colIndex']}")
                if fp.get("text"):
                    fp_parts.append(f"\"{fp['text'][:40]}\"")
                if fp.get("role"):
                    fp_parts.append(fp["role"])
                if fp.get("ariaLabel"):
                    fp_parts.append(f"aria={fp['ariaLabel']}")
                if fp.get("name"):
                    fp_parts.append(f"name={fp['name']}")
                lines.append(f"    Fingerprint: {', '.join(fp_parts)}")
            if meta.get("sourceContext"):
                sc = meta["sourceContext"]
                sc_parts = []
                if sc.get("component"):
                    sc_parts.append(sc["component"])
                if sc.get("section"):
                    sc_parts.append(sc["section"])
                if sc.get("landmark"):
                    sc_parts.append(sc["landmark"])
                if sc_parts:
                    lines.append(f"    Context: {' > '.join(sc_parts)}")
            if meta.get("selectedText"):
                lines.append(f'    Selected: "{meta["selectedText"]}"')
            if meta.get("accessibility"):
                lines.append(f"    A11y: {meta['accessibility']}")
            if meta.get("computedStyles"):
                lines.append(f"    Styles: {meta['computedStyles']}")
        except (json.JSONDecodeError, TypeError):
            pass
    return "\n".join(lines)


@mcp.tool()
async def create_github_issue(
    title: str,
    body: str,
    label: str,
) -> str:
    """Create a GitHub issue on the ui-ticket-mcp repository using the gh CLI. Requires gh to be installed and authenticated (https://cli.github.com). Label must be either 'bug' or 'enhancement'."""
    if label not in ("bug", "enhancement"):
        return "Invalid label. Must be 'bug' or 'enhancement'."
    if not shutil.which("gh"):
        return (
            "gh CLI is not installed. Install it to create GitHub issues:\n"
            "  - Windows: winget install --id GitHub.cli\n"
            "  - macOS: brew install gh\n"
            "  - Linux: https://github.com/cli/cli/blob/trunk/docs/install_linux.md\n"
            "\nAfter installing, run: gh auth login"
        )

    auth_result = await asyncio.to_thread(
        subprocess.run,
        ["gh", "auth", "status"],
        capture_output=True,
        stdin=subprocess.DEVNULL,
        timeout=15,
    )
    if auth_result.returncode != 0:
        return (
            "gh CLI is installed but not authenticated.\n"
            "Run: gh auth login\n"
            "Then try again."
        )

    cmd = [
        "gh", "issue", "create",
        "--repo", "0ics-srls/ui-ticket-mcp_public",
        "--title", title,
        "--body", body,
        "--label", label,
    ]

    result = await asyncio.to_thread(
        subprocess.run,
        cmd,
        capture_output=True,
        stdin=subprocess.DEVNULL,
        timeout=30,
    )

    if result.returncode != 0:
        err = result.stderr.decode().strip()
        return f"Failed to create issue: {err}"

    issue_url = result.stdout.decode().strip()
    return f"Created issue: {issue_url}"


# --------------- Background API server ---------------


def _start_api_server():
    """Start the FastAPI REST server in a background thread."""
    import uvicorn
    from .api import app

    port = int(os.environ.get("REVIEW_PORT", "3200"))
    uvicorn.run(app, host="0.0.0.0", port=port, log_level="warning")


# --------------- Main ---------------


def main():
    api_thread = threading.Thread(target=_start_api_server, daemon=True)
    api_thread.start()
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
