from __future__ import annotations

import os
import platform
from pathlib import Path
from typing import Any, ClassVar, Literal, cast

import platformdirs
import yaml
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

APP_NAME = "ocr-pipeline"
APP_AUTHOR = "aman"
CONFIG_ENV_VAR = "OCR_PIPELINE_CONFIG"
DEFAULT_CONFIG_FILENAME = "config.yaml"


def _get_platform_ignore_patterns() -> list[str]:
    """Get platform-specific ignore patterns for file watching."""
    patterns = [
        "*.tmp",
        "*.partial",
        "*.crdownload",
        "*.part",
        "~$*",  # MS Office temp files
        "*.swp",
        "*.swo",  # Vim swap files
    ]
    system = platform.system().lower()
    if system == "darwin":
        patterns.extend([".DS_Store", "._*", ".Spotlight-V100", ".Trashes", ".fseventsd"])
    elif system == "windows":
        patterns.extend(["Thumbs.db", "desktop.ini", "*.lnk", "$RECYCLE.BIN"])
    else:
        # Linux/Unix
        patterns.extend([".directory", "*.swp", "*.swo", ".git"])
    return patterns


class InputConfig(BaseSettings):
    paths: list[str] = Field(default_factory=lambda: ["~/Pictures"])
    patterns: list[str] = Field(
        default_factory=lambda: ["SCR-*.png", "*.jpg", "*.jpeg", "*.tiff", "*.bmp"]
    )
    recursive: bool = True
    exclude_patterns: list[str] = Field(default_factory=lambda: ["*.photoslibrary/*"])

    @field_validator("paths", mode="before")
    @classmethod
    def expand_paths(cls, v: list[str]) -> list[str]:
        return [os.path.expanduser(p) for p in v]


class PreprocessConfig(BaseSettings):
    deskew: bool = True
    denoise: bool = True
    clahe: bool = True
    adaptive_threshold: bool = True
    remove_lines: bool = True
    max_dimension: int = 4096


class OCRConfig(BaseSettings):
    engine: Literal["paddleocr", "tesseract", "auto"] = "tesseract"
    languages: list[str] = Field(default_factory=lambda: ["en", "latin"])
    use_gpu: bool = False
    use_angle_cls: bool = True
    det_db_thresh: float = 0.3
    det_db_box_thresh: float = 0.6
    det_db_unclip_ratio: float = 1.5
    rec_batch_num: int = 6
    cpu_threads: int = 4
    preprocess: PreprocessConfig = Field(default_factory=PreprocessConfig)


class ProcessingConfig(BaseSettings):
    workers: int = Field(default=4, ge=1)
    batch_size: int = Field(default=10, ge=1)
    retry_attempts: int = Field(default=2, ge=0)
    timeout_per_image: int = Field(default=120, ge=1)
    skip_existing: bool = True


class DetectorConfig(BaseSettings):
    enabled: bool = False
    model: str = ""
    confidence_threshold: float = Field(default=0.7, ge=0.0, le=1.0)


class FigureDetectorConfig(DetectorConfig):
    model: str = "lp://PubLayNet/faster_rcnn_R_50_FPN_3x/config"
    confidence_threshold: float = Field(default=0.7, ge=0.0, le=1.0)


class TableDetectorConfig(DetectorConfig):
    model: str = "microsoft/table-transformer-detection"
    confidence_threshold: float = Field(default=0.7, ge=0.0, le=1.0)


class CaptionDetectorConfig(DetectorConfig):
    model: str = "lp://PubLayNet/faster_rcnn_R_50_FPN_3x/config"
    confidence_threshold: float = Field(default=0.5, ge=0.0, le=1.0)


class DetectorsConfig(BaseSettings):
    figures: FigureDetectorConfig = Field(default_factory=FigureDetectorConfig)
    tables: TableDetectorConfig = Field(default_factory=TableDetectorConfig)
    captions: CaptionDetectorConfig = Field(default_factory=CaptionDetectorConfig)


class EntitiesConfig(BaseSettings):
    enabled: bool = False
    model: str = "en_core_sci_lg"
    types: list[str] = Field(
        default_factory=lambda: [
            "GENE",
            "PROTEIN",
            "CHEMICAL",
            "SPECIES",
            "DISEASE",
            "CELL_LINE",
            "ORGANISM",
            "CELL_TYPE",
        ]
    )
    merge_entities: bool = True


class CitationsConfig(BaseSettings):
    regex_enabled: bool = True
    grobid_enabled: bool = False
    grobid_url: str = "http://localhost:8070"
    grobid_timeout: int = 30


class ChunkingConfig(BaseSettings):
    chunk_size: int = 1000
    chunk_overlap: int = 200
    separators: list[str] = Field(default_factory=lambda: ["\n\n", "\n", ". ", " ", ""])
    keep_separator: bool = True


class FrontmatterConfig(BaseSettings):
    fields: list[str] = Field(
        default_factory=lambda: [
            "source_path",
            "source_hash",
            "timestamp",
            "ocr_engine",
            "ocr_confidence_mean",
            "language",
            "detected_entities",
            "entity_extraction_backend",
            "has_figures",
            "has_tables",
            "citations_found",
            "chunk_index",
            "total_chunks",
        ]
    )
    include_raw_text: bool = False


class OutputConfig(BaseSettings):
    write_consolidated: bool = False
    consolidated_filename: str = "all_ocr.md"
    base_directory: str = Field(
        default_factory=lambda: str(platformdirs.user_data_dir(APP_NAME, APP_AUTHOR))
    )
    format: str = "markdown"
    organize_by: Literal["date_run", "source_dir", "flat"] = "date_run"
    frontmatter: FrontmatterConfig = Field(default_factory=FrontmatterConfig)


class WatchConfig(BaseSettings):
    enabled: bool = True
    debounce_seconds: int = Field(default=5, ge=0)
    ignore_patterns: list[str] = Field(default_factory=_get_platform_ignore_patterns)
    db_path: str = Field(
        default_factory=lambda: str(
            Path(platformdirs.user_data_dir(APP_NAME, APP_AUTHOR)) / "processed_files.db"
        )
    )
    poll_interval: float = Field(default=1.0, gt=0)


class _BaseSettings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_nested_delimiter="__",
        extra="ignore",
    )


def user_config_dir() -> Path:
    """Per-user config directory for this machine."""
    return Path(platformdirs.user_config_dir(APP_NAME, APP_AUTHOR))


def _candidate_config_paths() -> list[Path]:
    """Config file locations in priority order."""
    candidates: list[Path] = []
    env_path = os.environ.get(CONFIG_ENV_VAR)
    if env_path:
        candidates.append(Path(env_path).expanduser())
    candidates.append(Path.cwd() / DEFAULT_CONFIG_FILENAME)
    candidates.append(user_config_dir() / DEFAULT_CONFIG_FILENAME)
    return candidates


def find_config_file() -> Path | None:
    """First config file that exists, or None when nothing is configured."""
    for candidate in _candidate_config_paths():
        if candidate.is_file():
            return candidate
    return None


def _load_yaml_config(path: str | Path | None = None) -> dict[str, Any]:
    """Load YAML config from an explicit path or the standard search paths."""
    if path is not None:
        config_path = Path(path)
        if not config_path.is_absolute():
            config_path = Path.cwd() / config_path
        if config_path.exists():
            with open(config_path) as f:
                return yaml.safe_load(f) or {}
        return {}
    found = find_config_file()
    if found is None:
        return {}
    with open(found) as f:
        return yaml.safe_load(f) or {}


def _build_settings_from_yaml(yaml_dict: dict[str, Any]) -> dict[str, Any]:
    """Convert YAML dict to properly typed nested models for Settings."""
    if not yaml_dict:
        return {}

    result = {}

    # Map YAML keys to model classes
    model_map = {
        "input": InputConfig,
        "ocr": OCRConfig,
        "processing": ProcessingConfig,
        "detectors": DetectorsConfig,
        "entities": EntitiesConfig,
        "citations": CitationsConfig,
        "chunking": ChunkingConfig,
        "output": OutputConfig,
        "watch": WatchConfig,
    }

    for key, model_class in model_map.items():
        if key in yaml_dict:
            data = yaml_dict[key]
            # Special handling for output config to transform frontmatter list -> dict
            if key == "output" and "frontmatter" in data and isinstance(data["frontmatter"], list):
                data = {**data, "frontmatter": {"fields": data["frontmatter"]}}
            result[key] = model_class(**data)

    return result


class Settings(_BaseSettings):
    input: InputConfig = Field(default_factory=InputConfig)
    ocr: OCRConfig = Field(default_factory=OCRConfig)
    processing: ProcessingConfig = Field(default_factory=ProcessingConfig)
    detectors: DetectorsConfig = Field(default_factory=DetectorsConfig)
    entities: EntitiesConfig = Field(default_factory=EntitiesConfig)
    citations: CitationsConfig = Field(default_factory=CitationsConfig)
    chunking: ChunkingConfig = Field(default_factory=ChunkingConfig)
    output: OutputConfig = Field(default_factory=OutputConfig)
    watch: WatchConfig = Field(default_factory=WatchConfig)

    # Store YAML config for manual override
    _yaml_config: ClassVar[dict[str, Any]] = {}

    def __init__(self, **kwargs):
        if not Settings._yaml_config:
            Settings._yaml_config = _load_yaml_config()

        # Build nested models from YAML
        yaml_models = _build_settings_from_yaml(Settings._yaml_config)
        super().__init__(**yaml_models, **kwargs)

    def update_from_yaml(self, path: str | Path) -> None:
        """Reload settings from a YAML file."""
        Settings._yaml_config = _load_yaml_config(path)
        # Re-initialize with new config
        yaml_models = _build_settings_from_yaml(Settings._yaml_config)
        for key, value in yaml_models.items():
            if hasattr(self, key):
                setattr(self, key, value)

    @property
    def output_dir(self) -> Path:
        return Path(self.output.base_directory).expanduser().resolve()

    @property
    def watch_db_path(self) -> Path:
        return Path(self.watch.db_path).expanduser().resolve()


class _LazySettings:
    """Import-safe handle for the global Settings.

    Instantiating Settings at import time read config.yaml from whatever
    directory the process started in, so `import ocr_pipeline` touched the
    filesystem and behaved differently depending on the caller's cwd. This
    proxy defers all of that until the settings are actually used.
    """

    def __init__(self) -> None:
        self._instance: Settings | None = None

    def _get_instance(self) -> Settings:
        if self._instance is None:
            self._instance = Settings()
        return self._instance

    def __getattr__(self, name: str) -> Any:
        return getattr(self._get_instance(), name)

    def __setattr__(self, name: str, value: Any) -> None:
        if name == "_instance":
            object.__setattr__(self, name, value)
        else:
            setattr(self._get_instance(), name, value)

    def __repr__(self) -> str:
        return repr(self._get_instance())


settings: Settings = cast(Settings, _LazySettings())
