"""
Core configuration for playwright-healer.

Designed for flexibility: env-var zero-config, programmatic, or YAML-based.
"""

from __future__ import annotations

import os
from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional, Sequence


class HealingStrategy(str, Enum):
    """Controls how healing stages are executed.

    SMART        - heuristic → dom_fuzzy → ai  (default, best cost/speed balance)
    HEURISTIC_ONLY  - free instant mutations only; never calls AI
    DOM_ONLY     - skip heuristic; use only AI with DOM context
    VISUAL_ONLY  - skip DOM; send screenshot to vision AI
    FULL         - all three stages always run in order
    PARALLEL     - DOM + Visual AI run concurrently; take highest-confidence result
    """

    SMART = "SMART"
    HEURISTIC_ONLY = "HEURISTIC_ONLY"
    DOM_ONLY = "DOM_ONLY"
    VISUAL_ONLY = "VISUAL_ONLY"
    FULL = "FULL"
    PARALLEL = "PARALLEL"


class CacheBackend(str, Enum):
    """Storage backend for the healed-selector cache."""

    MEMORY = "MEMORY"          # in-process LRU; fastest, ephemeral
    FILE = "FILE"              # JSON on disk; survives restarts
    REDIS = "REDIS"            # shared across CI workers


class AIProvider(str, Enum):
    """Supported AI provider identifiers."""

    OPENAI = "OPENAI"
    GROQ = "GROQ"
    GEMINI = "GEMINI"
    ANTHROPIC = "ANTHROPIC"
    DEEPSEEK = "DEEPSEEK"
    LOCAL = "LOCAL"            # any OpenAI-compatible local server (Ollama, LM Studio)


@dataclass
class AIProviderConfig:
    """Configuration for a single AI provider."""

    provider: AIProvider
    api_key: str
    model: str
    api_url: Optional[str] = None          # override base URL (required for LOCAL)
    temperature: float = 0.1
    max_tokens: int = 512
    timeout_s: float = 30.0
    vision_model: Optional[str] = None     # separate model for screenshot analysis

    # ------------------------------------------------------------------
    # Sensible defaults per provider
    # ------------------------------------------------------------------
    _DEFAULTS: dict = field(default_factory=dict, init=False, repr=False)

    def __post_init__(self) -> None:
        defaults = {
            AIProvider.OPENAI: {
                "api_url": "https://api.openai.com/v1/chat/completions",
                "model": "gpt-4o-mini",
                "vision_model": "gpt-4o",
            },
            AIProvider.GROQ: {
                "api_url": "https://api.groq.com/openai/v1/chat/completions",
                "model": "llama-3.3-70b-versatile",
            },
            AIProvider.GEMINI: {
                "api_url": "https://generativelanguage.googleapis.com/v1/models/{model}:generateContent",
                "model": "gemini-2.0-flash",
            },
            AIProvider.ANTHROPIC: {
                "api_url": "https://api.anthropic.com/v1/messages",
                "model": "claude-3-5-haiku-20241022",
                "vision_model": "claude-3-5-sonnet-20241022",
            },
            AIProvider.DEEPSEEK: {
                "api_url": "https://api.deepseek.com/v1/chat/completions",
                "model": "deepseek-chat",
            },
            AIProvider.LOCAL: {
                "api_url": "http://localhost:11434/v1/chat/completions",
                "model": "qwen2.5:7b",
            },
        }
        d = defaults.get(self.provider, {})
        if self.api_url is None:
            self.api_url = d.get("api_url")
        if not self.model:
            self.model = d.get("model", "")
        if self.vision_model is None:
            self.vision_model = d.get("vision_model", self.model)

    @classmethod
    def builder(cls) -> "_AIProviderConfigBuilder":
        return _AIProviderConfigBuilder()


class _AIProviderConfigBuilder:
    def __init__(self) -> None:
        self._provider: Optional[AIProvider] = None
        self._api_key: str = ""
        self._model: str = ""
        self._api_url: Optional[str] = None
        self._temperature: float = 0.1
        self._max_tokens: int = 512
        self._timeout_s: float = 30.0
        self._vision_model: Optional[str] = None

    def provider(self, p: AIProvider) -> "_AIProviderConfigBuilder":
        self._provider = p
        return self

    def api_key(self, k: str) -> "_AIProviderConfigBuilder":
        self._api_key = k
        return self

    def model(self, m: str) -> "_AIProviderConfigBuilder":
        self._model = m
        return self

    def api_url(self, u: str) -> "_AIProviderConfigBuilder":
        self._api_url = u
        return self

    def temperature(self, t: float) -> "_AIProviderConfigBuilder":
        self._temperature = t
        return self

    def vision_model(self, m: str) -> "_AIProviderConfigBuilder":
        self._vision_model = m
        return self

    def build(self) -> AIProviderConfig:
        if self._provider is None:
            raise ValueError("AIProviderConfig requires .provider()")
        if not self._api_key:
            raise ValueError("AIProviderConfig requires .api_key()")
        return AIProviderConfig(
            provider=self._provider,
            api_key=self._api_key,
            model=self._model,
            api_url=self._api_url,
            temperature=self._temperature,
            max_tokens=self._max_tokens,
            timeout_s=self._timeout_s,
            vision_model=self._vision_model,
        )


@dataclass
class CacheConfig:
    """Cache configuration."""

    backend: CacheBackend = CacheBackend.FILE
    max_size: int = 1000
    ttl_hours: float = 48.0           # default TTL
    min_ttl_hours: float = 1.0        # unstable selectors shortened to this
    max_ttl_hours: float = 720.0      # 30 days for ultra-stable selectors
    file_path: str = ".playwright_healer_cache.json"

    # Redis settings
    redis_host: str = "localhost"
    redis_port: int = 6379
    redis_password: Optional[str] = None
    redis_db: int = 0
    redis_key_prefix: str = "ph:"

    @classmethod
    def builder(cls) -> "_CacheConfigBuilder":
        return _CacheConfigBuilder()


class _CacheConfigBuilder:
    def __init__(self) -> None:
        self._cfg = CacheConfig()

    def backend(self, b: CacheBackend) -> "_CacheConfigBuilder":
        self._cfg.backend = b
        return self

    def max_size(self, n: int) -> "_CacheConfigBuilder":
        self._cfg.max_size = n
        return self

    def ttl_hours(self, h: float) -> "_CacheConfigBuilder":
        self._cfg.ttl_hours = h
        return self

    def file_path(self, p: str) -> "_CacheConfigBuilder":
        self._cfg.file_path = p
        return self

    def redis(
        self,
        host: str = "localhost",
        port: int = 6379,
        password: Optional[str] = None,
        db: int = 0,
    ) -> "_CacheConfigBuilder":
        self._cfg.redis_host = host
        self._cfg.redis_port = port
        self._cfg.redis_password = password
        self._cfg.redis_db = db
        self._cfg.backend = CacheBackend.REDIS
        return self

    def build(self) -> CacheConfig:
        return self._cfg


@dataclass
class HealerConfig:
    """Top-level configuration for playwright-healer."""

    # AI providers — first that works is used; subsequent ones are fallbacks
    providers: List[AIProviderConfig] = field(default_factory=list)

    # Healing behaviour
    strategy: HealingStrategy = HealingStrategy.SMART
    quick_timeout_ms: int = 500         # timeout to try original selector
    element_timeout_ms: int = 10_000    # total timeout for element lookup
    quick_check_retries: int = 1        # retries before entering healing pipeline

    # Heuristic healing toggles
    enable_heuristic: bool = True       # try ID/class mutations before AI
    enable_dom_fuzzy: bool = True       # fuzzy DOM match before AI
    enable_shadow_dom: bool = True      # penetrate shadow roots automatically
    enable_iframe: bool = True          # search inside iframes

    # Selector preference after healing
    prefer_aria: bool = True            # prefer get_by_role / get_by_text over CSS

    # Cache
    cache: CacheConfig = field(default_factory=CacheConfig)

    # Reporting
    report_dir: str = "playwright-healer-reports"
    report_html: bool = True
    report_json: bool = True
    console_logging: bool = True

    # Adaptive cache TTL
    adaptive_ttl: bool = True          # adjust TTL based on selector stability

    @classmethod
    def builder(cls) -> "_HealerConfigBuilder":
        return _HealerConfigBuilder()


class _HealerConfigBuilder:
    def __init__(self) -> None:
        self._cfg = HealerConfig()

    def add_provider(self, p: AIProviderConfig) -> "_HealerConfigBuilder":
        self._cfg.providers.append(p)
        return self

    def providers(self, ps: Sequence[AIProviderConfig]) -> "_HealerConfigBuilder":
        self._cfg.providers = list(ps)
        return self

    def strategy(self, s: HealingStrategy) -> "_HealerConfigBuilder":
        self._cfg.strategy = s
        return self

    def quick_timeout_ms(self, ms: int) -> "_HealerConfigBuilder":
        self._cfg.quick_timeout_ms = ms
        return self

    def element_timeout_ms(self, ms: int) -> "_HealerConfigBuilder":
        self._cfg.element_timeout_ms = ms
        return self

    def cache(self, c: CacheConfig) -> "_HealerConfigBuilder":
        self._cfg.cache = c
        return self

    def report_dir(self, d: str) -> "_HealerConfigBuilder":
        self._cfg.report_dir = d
        return self

    def prefer_aria(self, b: bool) -> "_HealerConfigBuilder":
        self._cfg.prefer_aria = b
        return self

    def enable_shadow_dom(self, b: bool) -> "_HealerConfigBuilder":
        self._cfg.enable_shadow_dom = b
        return self

    def console_logging(self, b: bool) -> "_HealerConfigBuilder":
        self._cfg.console_logging = b
        return self

    def adaptive_ttl(self, b: bool) -> "_HealerConfigBuilder":
        self._cfg.adaptive_ttl = b
        return self

    def build(self) -> HealerConfig:
        return self._cfg
