"""UI repair — detect selector drift and AI-propose fixes."""

from __future__ import annotations

import re
from pathlib import Path

import click

from maeris_mcp.drift_detector import detect_ui_drift
from maeris_mcp.drift_detector.models import DriftIssue, DriftKind, RepairResult
from maeris_mcp.drift_detector.ui_drift import (
    _iter_ui_files,
    _rank_files_by_tokens,
    _selector_verdict,
    _tokenize,
)
from maeris_mcp.tidy import SUPPORTED_EXTENSIONS, SKIP_DIRS


_MAX_SOURCE_CHARS = 40_000   # per-issue source snippet budget
# Regex to extract stable attribute values from source for the candidate list
_ATTR_VALUE_RE = re.compile(
    r'(?:data-[\w-]+|id|name|aria-label|placeholder)\s*=\s*["\']([^"\']{1,80})["\']',
    re.IGNORECASE,
)


def _read_source_context(
    selector: str,
    scan_root: Path,
    *,
    page_url: str = "",
    tc_name: str = "",
    ui_files: list[Path] | None = None,
) -> tuple[str, list[str]]:
    """Return (source_snippet, candidate_selectors) for the given selector.

    Uses page_url + tc_name tokens to rank files so the snippet is scoped
    to the page/feature being tested rather than the whole repo.

    Args:
        selector: The stale CSS selector.
        scan_root: Frontend source root.
        page_url: URL of the page the step runs on (used to scope files).
        tc_name: Testcase name (used as secondary scoping signal).
        ui_files: Pre-computed list of UI files (pass to avoid re-scanning).

    Returns:
        (source_snippet, candidate_selectors) where candidate_selectors is a
        deduplicated list of attribute values extracted from the relevant files.
    """
    # Build context tokens from page URL + testcase name (same logic as detection).
    tokens: set[str] = set()
    if page_url:
        parts = re.split(r"[/?#&=:_\-]+", page_url)
        tokens |= {t.lower() for t in parts if t and len(t) > 2 and not t.lower().startswith("http")}
    tokens |= _tokenize(tc_name)

    all_files = ui_files if ui_files is not None else _iter_ui_files(scan_root)

    # Priority 1: page-scoped files ranked by tokens.
    ranked = _rank_files_by_tokens(all_files, scan_root, tokens, limit=30)

    # Priority 2: fallback — files containing the selector's identifier hint.
    identifier_hint = selector.lstrip(".#[").split("=")[0].strip("\"'")
    if not ranked:
        ranked = [
            p for p in all_files
            if identifier_hint and identifier_hint in (
                p.read_text(encoding="utf-8", errors="ignore") if p.is_file() else ""
            )
        ][:30]

    # Priority 3: any files up to budget.
    candidates_for_snippet = ranked if ranked else all_files

    chunks: list[str] = []
    total = 0
    seen_attr_values: set[str] = set()
    candidate_selectors: list[str] = []

    for path in candidates_for_snippet:
        if total >= _MAX_SOURCE_CHARS:
            break
        if not path.is_file() or path.suffix not in SUPPORTED_EXTENSIONS:
            continue
        if any(part in SKIP_DIRS for part in path.parts):
            continue
        try:
            content = path.read_text(encoding="utf-8", errors="ignore")
        except OSError:
            continue
        rel = str(path.relative_to(scan_root))
        chunk = f"=== {rel} ===\n{content}\n"
        chunks.append(chunk)
        total += len(chunk)
        # Extract candidate selectors from this file.
        for val in _ATTR_VALUE_RE.findall(content):
            if val not in seen_attr_values:
                seen_attr_values.add(val)
                candidate_selectors.append(val)
                if len(candidate_selectors) >= 80:
                    break

    return "".join(chunks)[:_MAX_SOURCE_CHARS], candidate_selectors[:80]


async def repair_ui(
    client,
    scan_root: Path,
    model: str,
    dry_run: bool,
    yes: bool,
) -> RepairResult:
    """Detect UI selector drift and AI-propose selector fixes.

    Args:
        client: Active MaerisClient.
        scan_root: Frontend source root.
        model: Claude model for suggestions.
        dry_run: If True, print proposals but do not apply.
        yes: If True, auto-approve all suggestions without prompting.

    Returns:
        RepairResult with fixed/skipped/failed counts.
    """
    from maeris_mcp.ai_adapter.repair_suggestions import suggest_selector_fix

    report = await detect_ui_drift(client, scan_root)
    result = RepairResult()

    if not report.has_drift:
        return result

    # Pre-compute file list once for the whole repair session.
    ui_files = _iter_ui_files(scan_root)

    for issue in report.issues:
        if issue.kind != DriftKind.SELECTOR_MISSING:
            result.skipped += 1
            continue

        selector = issue.detail.get("selector", "")
        click.echo(
            f"\n  Stale selector in '{issue.resource_name}':\n"
            f"    {click.style(selector, fg='red')}"
        )

        try:
            step_or_comp = issue.detail.get("step") or issue.detail.get("component") or {}
            page_url = (
                step_or_comp.get("page_url")
                or step_or_comp.get("pageUrl")
                or step_or_comp.get("url")
                or ""
            )
            snippet, candidates = _read_source_context(
                selector, scan_root,
                page_url=page_url,
                tc_name=issue.resource_name,
                ui_files=ui_files,
            )
            fix = suggest_selector_fix(issue, snippet, model, candidate_selectors=candidates)
        except Exception as e:
            click.secho(f"  ✗ AI suggestion failed: {e}", fg="red")
            result.failed += 1
            continue

        new_sel = fix.get("new_selector", "")
        confidence = fix.get("confidence", 0.0)
        reasoning = fix.get("reasoning", "")

        if not new_sel or confidence < 0.5:
            click.secho(
                f"  ⚠ Low-confidence suggestion (confidence={confidence:.2f}) — skipping.",
                fg="yellow",
            )
            result.skipped += 1
            continue

        # Validate that the suggested selector actually exists in source.
        suggestion_verdict = _selector_verdict(
            new_sel, scan_root, context_tokens=None, files=ui_files,
        )

        click.echo(
            f"  Proposed: {click.style(new_sel, fg='green')}  "
            f"(confidence={confidence:.2f})\n"
            f"  Reason: {reasoning}"
        )

        if suggestion_verdict["verdict"] == "missing":
            click.secho(
                f"  ⚠ Warning: suggested selector not found in source — apply with caution.",
                fg="yellow",
            )
        elif suggestion_verdict["verdict"] == "present_global_only":
            click.secho(
                f"  ⚠ Suggested selector found in repo but not in this page's files.",
                fg="yellow",
            )

        if dry_run:
            click.secho("  [dry-run] Would apply this change.", fg="cyan")
            result.skipped += 1
            continue

        # Confirm each fix individually unless --yes was passed
        if not yes:
            try:
                if not click.confirm("  Apply this fix?", default=True):
                    click.echo("  Skipped.")
                    result.skipped += 1
                    continue
            except click.Abort:
                click.echo("  Skipped.")
                result.skipped += 1
                continue

        # Apply: update the testcase step via client
        tc_id = issue.resource_id
        source = issue.detail.get("source", "testcase_step")

        try:
            if source == "component":
                # Component path: flu_id is the component_id.
                flu_id = issue.detail.get("component_id", "")
                if not flu_id:
                    raise ValueError("component_id missing from drift issue detail")
                await client.edit_test_step({
                    "flu_id": flu_id,
                    "testcase_id": tc_id,
                    "css_selector": new_sel,
                })
            else:
                # Testcase-step path: extract flu_id from the step dict.
                step = issue.detail.get("step", {})
                flu_id = (
                    step.get("flu_id")
                    or step.get("_flu_id")
                    or step.get("id")
                    or step.get("step_id")
                    or ""
                )
                if flu_id:
                    await client.edit_test_step({
                        "flu_id": flu_id,
                        "testcase_id": tc_id,
                        "css_selector": new_sel,
                    })
                else:
                    # Fallback: no flu_id — use the bulk manual-edit endpoint.
                    updated_step = {**step, "css_selector": new_sel}
                    # Remove legacy selector key aliases to avoid conflicts.
                    updated_step.pop("selector", None)
                    updated_step.pop("cssSelector", None)
                    await client.edit_manual_testcase(
                        tc_id,
                        {"steps": [updated_step], "replace_all_steps": False},
                    )

            click.secho("  ✓ Applied.", fg="green")
            result.fixed += 1
        except Exception as e:
            click.secho(f"  ✗ Failed to apply: {e}", fg="red")
            result.failed += 1

    return result
