# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Extraction templates for Smart Intake — learn classification patterns from corrections.

Templates are built from approved review items where the user corrected the type.
They match text patterns and extract fields, boosting classification confidence.
"""

import json
import logging
import os
import re
import secrets
import threading
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)

_lock = threading.Lock()

MAX_TEMPLATES = 100
CONFIDENCE_BOOST = 0.2
COVERAGE_THRESHOLD = 0.6


def _data_dir() -> Path:
    try:
        from familiar.core.paths import DATA_DIR

        return DATA_DIR
    except ImportError:
        return Path.home() / ".familiar" / "data"


class ExtractionTemplate:
    """A learned extraction template for a content type."""

    def __init__(self, data: dict):
        self.id: str = data.get("id", "")
        self.content_type: str = data.get("content_type", "unknown")
        self.patterns: list[str] = data.get("patterns", [])
        self.field_patterns: dict[str, str] = data.get("field_patterns", {})
        self.created_at: str = data.get("created_at", "")
        self.use_count: int = data.get("use_count", 0)

    def match(self, text: str) -> Optional[dict]:
        """Try to match this template against text.

        Returns extracted fields if coverage exceeds threshold, else None.
        """
        if not self.patterns:
            return None

        text_lower = text.lower()
        matches = sum(1 for p in self.patterns if p.lower() in text_lower)
        coverage = matches / len(self.patterns) if self.patterns else 0.0

        if coverage < COVERAGE_THRESHOLD:
            return None

        # Extract fields using substring patterns
        fields = {}
        for field_name, pattern in self.field_patterns.items():
            match = re.search(re.escape(pattern), text, re.IGNORECASE)
            if match:
                # Try to extract the value after the pattern
                start = match.end()
                # Take up to 200 chars or until newline
                rest = text[start : start + 200].strip()
                value = rest.split("\n")[0].strip()
                # Clean up common delimiters
                value = value.lstrip(":").lstrip("-").strip()
                if value:
                    fields[field_name] = value

        return fields if fields else {"_matched": True}

    def to_dict(self) -> dict:
        return {
            "id": self.id,
            "content_type": self.content_type,
            "patterns": self.patterns,
            "field_patterns": self.field_patterns,
            "created_at": self.created_at,
            "use_count": self.use_count,
        }


class TemplateStore:
    """JSON store for extraction templates."""

    def __init__(self, data_file: Optional[Path] = None):
        self._file = data_file or (_data_dir() / "scribe_templates.json")
        self._ensure_file()

    def _ensure_file(self):
        if not self._file.exists():
            self._file.parent.mkdir(parents=True, exist_ok=True)
            self._write({"templates": [], "version": 1})

    def _read(self) -> dict:
        try:
            return json.loads(self._file.read_text(encoding="utf-8"))
        except Exception:
            return {"templates": [], "version": 1}

    def _write(self, data: dict):
        tmp = self._file.with_suffix(".tmp")
        tmp.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
        os.replace(str(tmp), str(self._file))
        try:
            os.chmod(str(self._file), 0o600)
        except OSError:
            pass

    def add_template(
        self,
        content_type: str,
        patterns: list[str],
        field_patterns: Optional[dict[str, str]] = None,
    ) -> str:
        """Add a new extraction template. Returns template ID."""
        template_id = f"tmpl_{secrets.token_hex(4)}"
        template = {
            "id": template_id,
            "content_type": content_type,
            "patterns": patterns,
            "field_patterns": field_patterns or {},
            "created_at": datetime.now(timezone.utc).isoformat(),
            "use_count": 0,
        }

        with _lock:
            data = self._read()
            templates = data.get("templates", [])
            if len(templates) >= MAX_TEMPLATES:
                # Remove least-used template
                templates.sort(key=lambda t: t.get("use_count", 0))
                templates.pop(0)
            templates.append(template)
            data["templates"] = templates
            self._write(data)

        return template_id

    def learn_from_correction(
        self,
        content_type: str,
        text: str,
        fields: dict,
    ) -> Optional[str]:
        """Learn a template from a corrected classification.

        Builds substring patterns from field values found in the original text.
        Returns template ID if learned, None otherwise.
        """
        patterns = []
        field_patterns = {}

        text_lower = text.lower()

        for field_name, field_value in fields.items():
            if not field_value or not isinstance(field_value, str):
                continue
            value_lower = str(field_value).lower()
            # Find the value in the text
            idx = text_lower.find(value_lower)
            if idx >= 0:
                # Look for a label before the value (within 50 chars)
                prefix = text[max(0, idx - 50) : idx].strip()
                # Take last line/word as pattern
                prefix_parts = prefix.split("\n")
                label = prefix_parts[-1].strip() if prefix_parts else ""
                if label:
                    patterns.append(label)
                    field_patterns[field_name] = label
                else:
                    patterns.append(field_value)

        if not patterns:
            return None

        return self.add_template(
            content_type=content_type,
            patterns=patterns,
            field_patterns=field_patterns,
        )

    def match_templates(self, text: str) -> Optional[tuple[str, dict, str]]:
        """Try all templates against text.

        Returns (content_type, extracted_fields, template_id) or None.
        """
        data = self._read()
        templates = data.get("templates", [])

        for tmpl_data in templates:
            template = ExtractionTemplate(tmpl_data)
            fields = template.match(text)
            if fields is not None:
                # Increment use count
                self._increment_use(tmpl_data["id"])
                return (template.content_type, fields, template.id)

        return None

    def _increment_use(self, template_id: str):
        """Increment use count for a template."""
        with _lock:
            data = self._read()
            for tmpl in data.get("templates", []):
                if tmpl.get("id") == template_id:
                    tmpl["use_count"] = tmpl.get("use_count", 0) + 1
                    break
            self._write(data)

    def list_templates(self, limit: int = 50) -> list[dict]:
        """List all templates."""
        data = self._read()
        templates = data.get("templates", [])
        templates.sort(key=lambda t: t.get("use_count", 0), reverse=True)
        return templates[:limit]

    def get_template(self, template_id: str) -> Optional[dict]:
        """Get a specific template by ID."""
        data = self._read()
        for tmpl in data.get("templates", []):
            if tmpl.get("id") == template_id:
                return tmpl
        return None

    def remove_template(self, template_id: str) -> bool:
        """Remove a template by ID."""
        with _lock:
            data = self._read()
            templates = data.get("templates", [])
            original_len = len(templates)
            data["templates"] = [t for t in templates if t.get("id") != template_id]
            if len(data["templates"]) < original_len:
                self._write(data)
                return True
        return False


_instance: Optional[TemplateStore] = None


def get_template_store() -> TemplateStore:
    """Singleton accessor."""
    global _instance
    if _instance is None:
        _instance = TemplateStore()
    return _instance
