"""
Source Patcher v2 — robustly rewrites broken selectors in source files.

Two-stage approach
==================
Stage 1 — File-wide string search (always runs when auto_patch_source=True):
    Searches the ENTIRE file content for the broken selector as a quoted
    string literal and replaces every occurrence with the healed selector.
    Unlike the old line-number approach this handles multi-line function
    calls correctly:

        Before:
            await healing_page.fill(
                "#userid",           ← selector on a different line than the call
                "Username field",
                value="tomsmith",
            )
        After:
            await healing_page.fill(
                "#username",
                "Username field",
                value="tomsmith",
            )

Stage 2 — AI-powered rewrite (runs when ai_patch_source=True):
    Sends the full file content plus the healing context to the configured
    AI provider.  The AI rewrites the broken selectors even in complex
    situations (variables, f-strings, role-based selectors that require a
    full method-call change, etc.).

Activation
----------
    HealerConfig(auto_patch_source=True)          # programmatic (Stage 1)
    HealerConfig(ai_patch_source=True)            # programmatic (Stage 1 + 2)
    PH_AUTO_PATCH_SOURCE=true                     # env var (Stage 1)
    PH_AI_PATCH_SOURCE=true                       # env var (Stage 1 + 2)
    pytest --ph-auto-patch-source                  # CLI (Stage 1)
    pytest --ph-ai-patch-source                    # CLI (Stage 1 + 2)

Safety
------
    - Patches are deduped (same file + old selector → one patch).
    - Files are read and written once per file, not once per selector.
    - Errors for one file never abort other files.
    - Set backup=True to create .bak copies before overwriting.
"""

from __future__ import annotations

import logging
import re
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional

if TYPE_CHECKING:
    from playwright_healer.ai_providers import ProviderChain

logger = logging.getLogger("playwright_healer.source_patcher")


# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------

@dataclass
class SourcePatch:
    """A pending replacement in a source file."""

    original_selector: str   # the broken selector (without quotes)
    healed_selector: str     # the working replacement (without quotes)
    file_path: str           # absolute path to the Python source file
    lineno: int              # 1-based line where the call started (informational only)
    description: str = ""   # human label from the test (for logging)
    stage: str = ""          # which healing stage found the fix


# ---------------------------------------------------------------------------
# Core patcher
# ---------------------------------------------------------------------------

class SourcePatcher:
    """
    Collects healing events and applies them at session end.

    Parameters
    ----------
    backup:
        When True a ``.bak`` copy is written before overwriting any file.
    """

    def __init__(self, backup: bool = False) -> None:
        self._queue: List[SourcePatch] = []
        self._backup = backup

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def queue(self, patch: SourcePatch) -> None:
        """Add a patch; silently drops exact duplicates (same file + old selector)."""
        for p in self._queue:
            if (
                p.file_path == patch.file_path
                and p.original_selector == patch.original_selector
            ):
                return
        self._queue.append(patch)

    def apply_all(self, console_logging: bool = True) -> List[SourcePatch]:
        """
        Stage 1: File-wide string literal search and replace.

        Searches the ENTIRE file content (not just one line) so multi-line
        calls are handled correctly.  All patches queued for the same file
        are applied in a single read-write pass.

        Returns the list of patches that were actually applied (i.e. the
        old selector string literal was found somewhere in the file).
        """
        if not self._queue:
            return []

        by_file: Dict[str, List[SourcePatch]] = {}
        for p in self._queue:
            by_file.setdefault(p.file_path, []).append(p)

        applied: List[SourcePatch] = []

        for file_path, patches in by_file.items():
            try:
                path = Path(file_path)
                if not path.exists() or not path.is_file():
                    logger.warning("Source patch skipped — file not found: %s", file_path)
                    continue

                content = path.read_text(encoding="utf-8")
                original_content = content

                for p in patches:
                    # Only patch if the healed selector is a plain selector
                    # string (CSS, XPath, simple text) that can be directly
                    # substituted.  Skip internal Playwright formats like
                    # "internal:role=..." which need a full call rewrite.
                    if not _is_substitutable(p.healed_selector):
                        logger.debug(
                            "Source patch deferred to AI stage — healed selector "
                            "%r not directly substitutable", p.healed_selector
                        )
                        continue

                    new_content = _replace_literal_in_file(
                        content, p.original_selector, p.healed_selector
                    )
                    if new_content != content:
                        content = new_content
                        applied.append(p)
                        if console_logging:
                            _print_patch(p, file_path)
                    else:
                        logger.debug(
                            "Source patch: literal %r not found in %s "
                            "(will be handled by AI stage if enabled)",
                            p.original_selector, file_path,
                        )

                if content != original_content:
                    if self._backup:
                        bak = path.with_suffix(path.suffix + ".bak")
                        bak.write_bytes(path.read_bytes())
                        logger.debug("Backup created: %s", bak)
                    path.write_text(content, encoding="utf-8")
                    n = sum(1 for p in patches if p in applied)
                    logger.info("Source patched: %s (%d selector(s) updated)", file_path, n)

            except OSError as exc:
                logger.warning("Source patch I/O error for %s: %s", file_path, exc)
            except Exception as exc:
                logger.warning("Source patch unexpected error for %s: %s", file_path, exc)

        # Keep un-applied patches in the queue so AI stage can use them
        applied_selectors = {p.original_selector for p in applied}
        self._queue = [p for p in self._queue if p.original_selector not in applied_selectors]
        return applied

    async def apply_with_ai(
        self,
        provider_chain: "ProviderChain",
        console_logging: bool = True,
    ) -> List[SourcePatch]:
        """
        Stage 2: AI-powered file rewrite.

        For each file that still has pending patches (not resolved by Stage 1),
        sends the full file content + healing context to the AI and asks it
        to rewrite the broken selectors.  Handles complex cases:
          - Role-based selectors that require a full method-call change
          - Selectors stored in variables
          - f-string or dynamically constructed selectors

        Returns the list of patches whose files were successfully rewritten.
        """
        if not self._queue:
            return []

        by_file: Dict[str, List[SourcePatch]] = {}
        for p in self._queue:
            by_file.setdefault(p.file_path, []).append(p)

        applied: List[SourcePatch] = []

        for file_path, patches in by_file.items():
            try:
                path = Path(file_path)
                if not path.exists() or not path.is_file():
                    logger.warning("AI patch skipped — file not found: %s", file_path)
                    continue

                content = path.read_text(encoding="utf-8")

                heal_lines = "\n".join(
                    f'  - {p.original_selector!r}  →  {p.healed_selector!r}'
                    f'  (healed by {p.stage}, element: {p.description})'
                    for p in patches
                )

                prompt = (
                    "You are a Python test automation engineer. "
                    "The test file below contains broken Playwright selectors that were "
                    "healed at runtime by playwright-healer.\n\n"
                    "Broken → healed mapping:\n"
                    f"{heal_lines}\n\n"
                    "Rules:\n"
                    "1. Replace ONLY the broken selector string literals with their "
                    "healed versions.\n"
                    "2. If the healed selector is a CSS/XPath string, just replace the "
                    "string value in the existing method call.\n"
                    "3. If the healed selector uses a Playwright ARIA method "
                    "(e.g. get_by_role, get_by_text), rewrite the call accordingly.\n"
                    "4. Do NOT change any other code, comments, or formatting.\n"
                    "5. Return ONLY the corrected file content — no markdown fences, "
                    "no explanation.\n\n"
                    f"FILE: {_rel_path(file_path)}\n"
                    "```python\n"
                    f"{content}\n"
                    "```"
                )

                corrected = await provider_chain.simple_completion(prompt)

                # Strip accidental markdown fences the AI might add
                corrected = _strip_markdown_fences(corrected)

                if corrected and corrected.strip() != content.strip():
                    if self._backup:
                        bak = path.with_suffix(path.suffix + ".bak")
                        bak.write_bytes(path.read_bytes())
                        logger.debug("AI patch backup: %s", bak)
                    path.write_text(corrected, encoding="utf-8")
                    applied.extend(patches)
                    logger.info(
                        "AI source patch applied: %s (%d selector(s))",
                        file_path, len(patches),
                    )
                    if console_logging:
                        for p in patches:
                            _print_patch(p, file_path, via_ai=True)
                else:
                    logger.debug("AI returned unchanged content for %s", file_path)

            except Exception as exc:
                logger.warning("AI patch failed for %s: %s", file_path, exc)

        self._queue.clear()
        return applied

    @property
    def pending(self) -> List[SourcePatch]:
        """Return a copy of all patches not yet applied."""
        return list(self._queue)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _is_substitutable(healed: str) -> bool:
    """
    Return True when ``healed`` is a plain CSS/XPath/text selector that can
    be directly substituted as a quoted string literal in the test file.

    Rejects Playwright internal formats such as ``internal:role=...`` which
    require a full method-call rewrite (handled by the AI stage instead).
    """
    if healed.startswith("internal:"):
        return False
    # Role selectors in ROLE::NAME format need a get_by_role() call rewrite
    # — defer to AI stage if the test currently uses a css/xpath call site.
    # Simple CSS (#id, .class, tag[attr]) and XPath (//, ./) → substitutable.
    return True


def _replace_literal_in_file(content: str, old: str, new: str) -> str:
    """
    Replace every occurrence of ``old`` as a quoted string literal anywhere
    in ``content``.  Handles both single-quote and double-quote delimiters.

    This operates on the ENTIRE file (not line-by-line), so multi-line
    function calls are handled correctly regardless of which line the
    selector argument falls on.
    """
    for quote in ('"', "'"):
        token = f"{quote}{old}{quote}"
        replacement = f"{quote}{new}{quote}"
        content = content.replace(token, replacement)
    return content


def _strip_markdown_fences(text: str) -> str:
    """Remove ```python ... ``` or ``` ... ``` wrappers if the AI added them."""
    text = text.strip()
    # Remove leading fence
    text = re.sub(r"^```(?:python)?\s*\n", "", text)
    # Remove trailing fence
    text = re.sub(r"\n```\s*$", "", text)
    return text


def _print_patch(patch: SourcePatch, file_path: str, via_ai: bool = False) -> None:
    """Print a Rich-formatted one-liner about an applied patch."""
    rel = _rel_path(file_path)
    label = "AI PATCHED" if via_ai else "SOURCE PATCHED"
    try:
        from rich.console import Console
        _c = Console(highlight=False)
        _c.print(
            f"  [bold cyan]{label}[/bold cyan]  "
            f"[dim]{rel}[/dim]  "
            f"[red]{patch.original_selector!r}[/red] "
            f"→ [bold green]{patch.healed_selector!r}[/bold green]  "
            f"[dim]via {patch.stage}[/dim]"
        )
    except ImportError:
        print(
            f"  [{label}] {rel}  "
            f"{patch.original_selector!r} -> {patch.healed_selector!r}"
            f"  (via {patch.stage})"
        )


def _rel_path(file_path: str) -> str:
    """Return path relative to cwd, falling back to the absolute path."""
    try:
        return str(Path(file_path).relative_to(Path.cwd()))
    except ValueError:
        return file_path
