"""
Main FormCrawler class for navigating and filling multi-step web forms.

This module contains the core crawler implementation that orchestrates
field handlers and action executors to complete form workflows.
"""

from pathlib import Path
from typing import Any

from playwright.sync_api import (
    Error as PlaywrightError,
)
from playwright.sync_api import (
    Page,
    sync_playwright,
)
from playwright.sync_api import (
    TimeoutError as PlaywrightTimeoutError,
)

from .actions import get_action_executor
from .browser_utils import DEFAULT_USER_AGENT, handle_cookie_consent
from .diagnostics import CrawlContext, capture_page_diagnostics, write_diagnostic_json
from .exceptions import CrawlerError, CrawlerTimeoutError
from .field_handlers import get_field_handler
from .models import CrawlResult, DebugMode, InMemoryCrawlResult, Instructions, StepDefinition
from .utils import setup_logger

logger = setup_logger(__name__)


class FormCrawler:
    """
    Crawler for navigating and filling multi-step web forms.

    This class provides the main interface for automating form completion
    based on JSON instructions. It handles:
    - Multi-step wizard forms
    - Various field types (text, dropdown, radio, etc.)
    - Navigation between form steps
    - Waiting for AJAX-loaded content
    - Saving final page HTML and screenshots
    """

    def __init__(self, headless: bool = True) -> None:
        """
        Initialize the crawler.

        Args:
            headless: Whether to run browser in headless mode

        """
        self._headless: bool = headless

    def crawl(
        self,
        url: str,
        input_data: dict[str, Any],
        instructions: dict[str, Any],
        output_dir: Path | None = None,
        debug_mode: DebugMode = DebugMode.NONE,
    ) -> CrawlResult | InMemoryCrawlResult:
        """
        Navigate and fill the form, returning results.

        Args:
            url: Starting URL where the form begins
            input_data: Field values to fill (key-value pairs)
            instructions: Navigation and field selectors (JSON structure)
            output_dir: Directory to save HTML and screenshots. If None, returns in-memory result.
            debug_mode: Screenshot mode (none, start, end, all)

        Returns:
            CrawlResult with paths to saved files when output_dir is provided,
            InMemoryCrawlResult with in-memory data when output_dir is None.

        """
        validated_instructions = Instructions.model_validate(instructions)
        self._debug_mode: DebugMode = debug_mode
        self._output_dir: Path | None = output_dir
        self._screenshot_counter: int = 0
        self._current_step_name: str | None = None
        self._in_memory_screenshots: list[bytes] = []

        if output_dir is not None:
            output_dir.mkdir(parents=True, exist_ok=True)

        debug_enabled = debug_mode != DebugMode.NONE
        self._context: CrawlContext = CrawlContext(
            output_dir=output_dir,
            debug_enabled=debug_enabled,
        )

        logger.info(f"Starting crawl: url='{url}' steps={len(validated_instructions.steps)} debug='{debug_mode.value}'")

        with sync_playwright() as playwright:
            browser = playwright.chromium.launch(headless=self._headless)

            browser_config = validated_instructions.browser_config
            viewport_width = browser_config.viewport_width if browser_config else 1920
            viewport_height = browser_config.viewport_height if browser_config else 1080
            user_agent = (
                browser_config.user_agent if browser_config and browser_config.user_agent else DEFAULT_USER_AGENT
            )

            context = browser.new_context(
                viewport={"width": viewport_width, "height": viewport_height},
                user_agent=user_agent,
            )
            page = context.new_page()

            self._setup_event_handlers(page=page)

            try:
                page.goto(url=url, wait_until="networkidle")
                logger.info(f"Navigated to starting URL: url='{url}'")

                if validated_instructions.cookie_consent:
                    handle_cookie_consent(
                        page=page,
                        config=validated_instructions.cookie_consent,
                    )

                if debug_mode in (DebugMode.START, DebugMode.ALL):
                    self._take_debug_screenshot(page=page, label="start")

                steps_completed = 0
                for step in validated_instructions.steps:
                    self._process_step(
                        page=page,
                        step=step,
                        input_data=input_data,
                    )
                    steps_completed += 1

                return self._capture_final_page(
                    page=page,
                    instructions=validated_instructions,
                    output_dir=output_dir,
                    steps_completed=steps_completed,
                    url=url,
                    input_data=input_data,
                    raw_instructions=instructions,
                )

            except CrawlerError as e:
                if self._context.debug_enabled:
                    self._capture_error_diagnostics(
                        page=page,
                        error=e,
                    )
                raise

            finally:
                context.close()
                browser.close()

    def _setup_event_handlers(self, page: Page) -> None:
        """Set up console and network event handlers for diagnostic capture."""

        def on_console(msg: Any) -> None:
            self._context.console_messages.append(
                {
                    "type": msg.type,
                    "text": msg.text,
                }
            )

        def on_request_failed(request: Any) -> None:
            self._context.failed_requests.append(
                {
                    "url": request.url,
                    "method": request.method,
                    "failure": request.failure,
                }
            )

        page.on("console", on_console)
        page.on("requestfailed", on_request_failed)

    def _capture_error_diagnostics(
        self,
        page: Page,
        error: CrawlerError,
    ) -> None:
        """Capture page diagnostics when an error occurs."""
        failed_selector = getattr(error, "selector", None)
        step_name = self._current_step_name

        label = step_name if step_name else "unknown"

        diagnostics = capture_page_diagnostics(
            page=page,
            context=self._context,
            failed_selector=failed_selector,
            step_name=step_name,
            label=label,
        )

        error.with_diagnostics(diagnostics)

        if self._output_dir is not None:
            write_diagnostic_json(
                output_dir=self._output_dir,
                error=error,
                diagnostics=diagnostics,
            )

    def _take_debug_screenshot(self, page: Page, label: str) -> None:
        """Take a debug screenshot with incrementing counter."""
        self._screenshot_counter += 1

        if self._output_dir is not None:
            filename = f"debug_{self._screenshot_counter:03d}_{label}.png"
            screenshot_path = self._output_dir / filename
            page.screenshot(path=str(screenshot_path), full_page=True)
            logger.info(f"Debug screenshot: path='{screenshot_path}'")
        else:
            screenshot_bytes = page.screenshot(full_page=True)
            self._in_memory_screenshots.append(screenshot_bytes)
            logger.info(f"Debug screenshot captured in-memory: label='{label}'")

    def _process_step(
        self,
        page: Page,
        step: StepDefinition,
        input_data: dict[str, Any],
    ) -> None:
        """Process a single form step."""
        self._current_step_name = step.name
        logger.info(f"Processing step: name='{step.name}'")

        try:
            page.wait_for_selector(
                selector=step.wait_for,
                state="visible",
                timeout=step.timeout_ms,
            )
            logger.info(f"Wait condition met: selector='{step.wait_for}' step='{step.name}'")
        except (PlaywrightTimeoutError, PlaywrightError) as e:
            raise CrawlerTimeoutError(
                selector=step.wait_for,
                step_name=step.name,
                timeout_ms=step.timeout_ms,
            ) from e

        for field in step.fields:
            handler = get_field_handler(field_type=field.type, step_name=step.name)
            handler.handle(
                page=page,
                field=field,
                input_data=input_data,
                step_name=step.name,
            )

            if self._debug_mode == DebugMode.ALL:
                label = f"{step.name}_{field.data_key}"
                self._take_debug_screenshot(page=page, label=label)

        executor = get_action_executor(action_type=step.next_action.type, step_name=step.name)
        executor.execute(
            page=page,
            action=step.next_action,
            step_name=step.name,
            input_data=input_data,
        )

        if self._debug_mode == DebugMode.ALL:
            self._take_debug_screenshot(page=page, label=f"{step.name}_after_action")

        logger.info(f"Step completed: name='{step.name}'")

    def _capture_final_page(
        self,
        page: Page,
        instructions: Instructions,
        output_dir: Path | None,
        steps_completed: int,
        url: str,
        input_data: dict[str, Any],
        raw_instructions: dict[str, Any],
    ) -> CrawlResult | InMemoryCrawlResult:
        """Capture the final result page."""
        final_page = instructions.final_page

        try:
            page.wait_for_selector(
                selector=final_page.wait_for,
                state="visible",
                timeout=final_page.timeout_ms,
            )
            logger.info(f"Final page loaded: selector='{final_page.wait_for}'")
        except (PlaywrightTimeoutError, PlaywrightError) as e:
            raise CrawlerTimeoutError(
                selector=final_page.wait_for,
                step_name="final_page",
                timeout_ms=final_page.timeout_ms,
            ) from e

        if self._debug_mode in (DebugMode.END, DebugMode.ALL):
            self._take_debug_screenshot(page=page, label="end")

        html_content = page.content()
        final_url = page.url

        if output_dir is not None:
            html_path = output_dir / "result.html"
            html_path.write_text(data=html_content, encoding="utf-8")
            logger.info(f"Saved HTML: path='{html_path}'")

            screenshot_path = output_dir / "result.png"
            if final_page.screenshot_selector:
                locator = page.locator(final_page.screenshot_selector)
                locator.screenshot(path=str(screenshot_path))
            else:
                page.screenshot(path=str(screenshot_path), full_page=True)
            logger.info(f"Saved screenshot: path='{screenshot_path}'")

            return CrawlResult(
                html_path=html_path,
                screenshot_path=screenshot_path,
                final_url=final_url,
                steps_completed=steps_completed,
            )

        if final_page.screenshot_selector:
            locator = page.locator(final_page.screenshot_selector)
            final_screenshot = locator.screenshot()
        else:
            final_screenshot = page.screenshot(full_page=True)

        self._in_memory_screenshots.append(final_screenshot)
        logger.info("Captured final screenshot in-memory")

        return InMemoryCrawlResult(
            url=url,
            input_data=input_data,
            instructions=raw_instructions,
            screenshots=self._in_memory_screenshots,
            html=html_content,
            final_url=final_url,
            steps_completed=steps_completed,
        )
