from __future__ import annotations

import asyncio
import os
import re
import shutil
import tempfile
from pathlib import Path
from typing import Any

try:
    from playwright.async_api import async_playwright
except ImportError:  # pragma: no cover - optional in local dev
    async_playwright = None

try:
    from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
except ImportError:  # pragma: no cover - optional in local dev
    ClaudeAgentOptions = None
    ResultMessage = None
    query = None


QUICK_CHECKS: dict[str, dict[str, Any]] = {
    "box-shadow": {"pattern": r"box-shadow", "max_count": 0, "penalty": 2, "description": "No drop shadows allowed"},
    "gradient": {"pattern": r"linear-gradient|radial-gradient", "max_count": 0, "penalty": 2, "description": "No gradients in artifact chrome"},
    "font-weight-heavy": {
        "pattern": r"font-weight:\s*(?:[6-9]00|bold)",
        "max_count": 0,
        "penalty": 1,
        "description": "Only font-weight 400 and 500 should ship",
    },
    "uppercase": {"pattern": r"text-transform:\s*uppercase", "max_count": 0, "penalty": 1, "description": "Sentence case always"},
    "small-font": {"pattern": r"font-size:\s*(?:[0-9]|10)\s*px", "max_count": 0, "penalty": 1, "description": "Minimum font size 11px"},
    "position-fixed": {"pattern": r"position:\s*fixed", "max_count": 0, "penalty": 3, "description": "No position:fixed in artifacts"},
}


def sdk_backend_available() -> bool:
    return bool(query and ClaudeAgentOptions and ResultMessage and _find_sdk_cli_path())


def _find_sdk_cli_path() -> str | None:
    configured = os.getenv("CLAUDE_CLI_PATH", "").strip()
    if configured:
        return configured
    discovered = shutil.which("claude")
    if discovered:
        return discovered
    for candidate in (
        Path("/usr/local/bin/claude"),
        Path.home() / ".npm-global/bin" / "claude",
        Path.home() / ".local" / "bin" / "claude",
    ):
        if candidate.exists():
            return str(candidate)
    return None


async def run_quick_checks(html: str) -> tuple[int, list[str]]:
    deductions = 0
    warnings: list[str] = []
    for name, rule in QUICK_CHECKS.items():
        matches = re.findall(str(rule["pattern"]), html or "", re.IGNORECASE)
        count = len(matches)
        if count > int(rule["max_count"]):
            deductions += int(rule["penalty"]) * count
            warnings.append(f"{name}: found {count} instance(s) ({rule['description']})")
    return deductions, warnings


async def run_visual_checks(html: str) -> tuple[int, list[str]]:
    if async_playwright is None:
        raise RuntimeError("Playwright async API is not available.")

    deductions = 0
    warnings: list[str] = []

    with tempfile.NamedTemporaryFile(mode="w", suffix=".html", encoding="utf-8", delete=False) as handle:
        handle.write(html or "")
        html_path = Path(handle.name)

    try:
        async with async_playwright() as playwright:
            browser = await playwright.chromium.launch()
            try:
                for viewport in (
                    {"name": "phone", "width": 375, "height": 812},
                    {"name": "tablet", "width": 768, "height": 1024},
                    {"name": "desktop", "width": 1200, "height": 800},
                ):
                    page = await browser.new_page(viewport={"width": viewport["width"], "height": viewport["height"]})
                    try:
                        await page.goto(html_path.as_uri())
                        await page.wait_for_timeout(1000)
                        has_overflow = await page.evaluate("document.documentElement.scrollWidth > document.documentElement.clientWidth")
                        if has_overflow:
                            deductions += 3
                            warnings.append(f"Horizontal overflow at {viewport['name']} ({viewport['width']}px)")

                        zero_height_count = await page.evaluate(
                            """
                            [...document.querySelectorAll('section, div, article')]
                                .filter(el => el.offsetHeight === 0 && el.children.length > 0).length
                            """
                        )
                        if int(zero_height_count or 0) > 0:
                            deductions += 2
                            warnings.append(f"{int(zero_height_count)} zero-height container(s) at {viewport['name']}")

                        clipped_count = await page.evaluate(
                            """
                            [...document.querySelectorAll('*')]
                                .filter(el => {
                                    const style = getComputedStyle(el);
                                    return style.overflow === 'hidden' &&
                                           el.scrollHeight > el.clientHeight + 2 &&
                                           el.clientHeight > 0 && el.clientHeight < 500;
                                }).length
                            """
                        )
                        if int(clipped_count or 0) > 2:
                            deductions += 2
                            warnings.append(f"{int(clipped_count)} clipped element(s) at {viewport['name']}")
                    finally:
                        await page.close()
            finally:
                await browser.close()
    finally:
        html_path.unlink(missing_ok=True)

    return deductions, warnings


def _extract_json_object(raw_text: str) -> str:
    start = raw_text.find("{")
    if start == -1:
        return ""
    depth = 0
    for index in range(start, len(raw_text)):
        char = raw_text[index]
        if char == "{":
            depth += 1
        elif char == "}":
            depth -= 1
            if depth == 0:
                return raw_text[start : index + 1]
    return ""


async def run_sdk_review(
    prompt: str,
    *,
    system_prompt: str = "",
    model: str = "haiku",
    timeout_seconds: int = 20,
) -> dict[str, Any]:
    if not sdk_backend_available():
        raise RuntimeError("Claude Agent SDK or Claude Code CLI is not available.")

    cli_path = _find_sdk_cli_path()
    if not cli_path:
        raise RuntimeError("Claude Code CLI was not found for the Agent SDK backend.")

    normalized_model = (model or "").strip().lower()
    if "haiku" in normalized_model:
        sdk_model = "claude-haiku-4-5"
    elif "opus" in normalized_model:
        sdk_model = "claude-opus-4-1"
    else:
        sdk_model = "claude-sonnet-4-5"
    options = ClaudeAgentOptions(
        model=sdk_model,
        system_prompt=system_prompt or None,
        tools=[],
        allowed_tools=[],
        cwd=os.getcwd(),
        cli_path=cli_path,
        permission_mode="default",
        max_turns=2,
        include_partial_messages=False,
    )

    async def collect() -> str:
        final_result = ""
        async for event in query(prompt=prompt, options=options):
            if ResultMessage is not None and isinstance(event, ResultMessage) and event.result:
                final_result = str(event.result)
        return final_result

    raw_text = await asyncio.wait_for(collect(), timeout_seconds)
    candidate = _extract_json_object(raw_text) or raw_text.strip()
    if not candidate:
        raise ValueError("Agent SDK review returned no JSON.")
    return {"raw_text": raw_text, "parsed": candidate}
