# 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

"""
Content classification engine for Smart Intake.

Detects content type from extracted text using keyword heuristics first
(fast, no LLM needed), then optional LLM refinement for ambiguous cases.

Content types map directly to routing destinations in the router module.
"""

import logging
import os
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional

logger = logging.getLogger(__name__)


class ContentType(Enum):
    """Detected content type — each maps to a routing destination."""

    DONATION_RECEIPT = "donation_receipt"
    GRANT_DOCUMENT = "grant_document"
    INVOICE = "invoice"
    EVENT_FLYER = "event_flyer"
    LEGISLATION = "legislation"
    MEETING_NOTES = "meeting_notes"
    BUSINESS_CARD = "business_card"
    LETTER = "letter"
    SPREADSHEET_DATA = "spreadsheet_data"
    MEDICAL_RECORD = "medical_record"
    CASE_NOTE = "case_note"
    RECIPE = "recipe"
    INVENTORY_LIST = "inventory_list"
    GENERAL_DOCUMENT = "general_document"
    PHOTO = "photo"
    AUDIO_TRANSCRIPT = "audio_transcript"
    UNKNOWN = "unknown"


@dataclass
class ClassificationResult:
    """Result of content classification."""

    content_type: ContentType
    confidence: float  # 0.0 to 1.0
    extracted_text: str
    extracted_fields: dict = field(default_factory=dict)
    source_file: str = ""
    method: str = "keyword"  # "keyword", "llm", "manual", "template"
    needs_review: bool = False  # True if confidence < threshold or type UNKNOWN
    suggested_route: str = ""  # Filled by router


# ── Keyword patterns for fast classification ──────────────────────────

# Each pattern: (content_type, keywords_that_must_match, min_matches)
_KEYWORD_PATTERNS: list[tuple[ContentType, list[str], int]] = [
    (
        ContentType.DONATION_RECEIPT,
        [
            "donation",
            "gift",
            "tax-deductible",
            "tax deductible",
            "charitable",
            "receipt",
            "contribution",
            "donor",
            "501(c)",
            "ein",
            "thank you for your",
            "deductible",
        ],
        2,
    ),
    (
        ContentType.GRANT_DOCUMENT,
        [
            "grant",
            "proposal",
            "funding",
            "rfp",
            "request for proposal",
            "narrative",
            "budget justification",
            "cfda",
            "funder",
            "award",
            "deliverables",
            "reporting requirements",
        ],
        2,
    ),
    (
        ContentType.INVOICE,
        [
            "invoice",
            "bill to",
            "amount due",
            "payment terms",
            "subtotal",
            "total due",
            "net 30",
            "net 60",
            "remit to",
            "purchase order",
            "qty",
            "unit price",
        ],
        2,
    ),
    (
        ContentType.EVENT_FLYER,
        [
            "event",
            "join us",
            "rsvp",
            "register",
            "tickets",
            "venue",
            "doors open",
            "keynote",
            "schedule",
            "agenda",
            "workshop",
            "conference",
            "gala",
            "fundraiser",
        ],
        2,
    ),
    (
        ContentType.LEGISLATION,
        [
            "bill",
            "act",
            "section",
            "subsection",
            "whereas",
            "enacted",
            "legislature",
            "statute",
            "committee",
            "hearing",
            "testimony",
            "amendment",
            "resolution",
        ],
        3,
    ),
    (
        ContentType.MEETING_NOTES,
        [
            "minutes",
            "attendees",
            "action items",
            "agenda",
            "meeting",
            "discussed",
            "motion",
            "seconded",
            "quorum",
            "adjourned",
            "next meeting",
        ],
        2,
    ),
    (
        ContentType.BUSINESS_CARD,
        [
            "email",
            "phone",
            "tel",
            "fax",
            "website",
            "www",
            "director",
            "manager",
            "ceo",
            "founder",
        ],
        3,
    ),
    (
        ContentType.MEDICAL_RECORD,
        [
            "patient",
            "diagnosis",
            "medication",
            "prescription",
            "treatment",
            "physician",
            "clinic",
            "hospital",
            "medical record",
            "health",
            "symptoms",
            "vitals",
        ],
        3,
    ),
    (
        ContentType.CASE_NOTE,
        [
            "case",
            "client",
            "assessment",
            "referral",
            "intake",
            "progress note",
            "service plan",
            "discharge",
            "presenting problem",
            "intervention",
        ],
        2,
    ),
    (
        ContentType.RECIPE,
        [
            "ingredients",
            "instructions",
            "preheat",
            "servings",
            "prep time",
            "cook time",
            "tablespoon",
            "teaspoon",
            "cups",
            "oven",
            "bake",
            "simmer",
        ],
        3,
    ),
    (
        ContentType.INVENTORY_LIST,
        [
            "inventory",
            "stock",
            "quantity",
            "sku",
            "item",
            "warehouse",
            "on hand",
            "reorder",
            "unit cost",
        ],
        2,
    ),
    (
        ContentType.LETTER,
        [
            "dear",
            "sincerely",
            "regards",
            "to whom it may concern",
            "enclosed",
            "attached",
            "please find",
        ],
        2,
    ),
]


def classify_by_keywords(text: str) -> ClassificationResult:
    """
    Fast keyword-based classification. No LLM needed.

    Returns the best-matching content type with confidence score.
    """
    if not text or not text.strip():
        return ClassificationResult(
            content_type=ContentType.UNKNOWN,
            confidence=0.0,
            extracted_text="",
        )

    text_lower = text.lower()
    best_type = ContentType.GENERAL_DOCUMENT
    best_score = 0.0

    for content_type, keywords, min_matches in _KEYWORD_PATTERNS:
        matches = sum(1 for kw in keywords if kw in text_lower)
        if matches >= min_matches:
            # Score = matches / total keywords, weighted by min_matches threshold
            score = min(1.0, matches / len(keywords) * 1.5)
            if score > best_score:
                best_score = score
                best_type = content_type

    # Low confidence for general fallback
    if best_type == ContentType.GENERAL_DOCUMENT:
        best_score = 0.3

    return ClassificationResult(
        content_type=best_type,
        confidence=round(best_score, 2),
        extracted_text=text,
        method="keyword",
    )


def classify_with_llm(
    text: str,
    filepath: Optional[str] = None,
    timeout: float = 30.0,
) -> Optional[ClassificationResult]:
    """
    LLM-based classification for ambiguous content.

    Uses a lightweight Ollama model to classify content type and
    extract structured fields. Only called when keyword confidence < 0.6.

    Returns None if LLM is unavailable.
    """
    import json as json_mod

    ollama_host = os.environ.get("OLLAMA_HOST", "http://localhost:11434")

    type_names = [ct.value for ct in ContentType if ct != ContentType.UNKNOWN]

    prompt = f"""Classify this document and extract key fields.

Content types: {", ".join(type_names)}

Document text:
---
{text[:3000]}
---

Respond with JSON only:
{{"content_type": "<type>", "confidence": <0.0-1.0>, "fields": {{"<key>": "<value>"}}}}

Fields to extract based on type:
- donation_receipt: donor_name, amount, date, organization
- grant_document: grant_name, funder, amount, deadline
- invoice: vendor, amount, due_date, invoice_number
- event_flyer: event_name, date, venue, organizer
- legislation: bill_number, title, jurisdiction, status
- business_card: name, title, organization, email, phone
- recipe: name, servings, prep_time, cook_time
- For other types: extract any relevant structured fields"""

    try:
        import httpx

        resp = httpx.post(
            f"{ollama_host}/api/chat",
            json={
                "model": os.environ.get("FAMILIAR_CLASSIFY_MODEL", "qwen2.5:3b"),
                "messages": [{"role": "user", "content": prompt}],
                "stream": False,
                "format": "json",
            },
            timeout=timeout,
        )
        if resp.status_code != 200:
            return None

        content = resp.json().get("message", {}).get("content", "")
        if not content:
            return None

        parsed = json_mod.loads(content)
        ct_value = parsed.get("content_type", "unknown")
        try:
            content_type = ContentType(ct_value)
        except ValueError:
            content_type = ContentType.GENERAL_DOCUMENT

        return ClassificationResult(
            content_type=content_type,
            confidence=min(1.0, max(0.0, float(parsed.get("confidence", 0.5)))),
            extracted_text=text,
            extracted_fields=parsed.get("fields", {}),
            source_file=filepath or "",
            method="llm",
        )
    except Exception as e:
        logger.debug("LLM classification failed: %s", e)
        return None


def classify(
    text: str,
    filepath: Optional[str] = None,
    use_llm: bool = True,
    llm_threshold: float = 0.6,
) -> ClassificationResult:
    """
    Tiered classification: keyword first, LLM for ambiguous cases.

    Args:
        text: Extracted text content to classify.
        filepath: Source file path (for context).
        use_llm: Whether to attempt LLM classification on low confidence.
        llm_threshold: Keyword confidence below this triggers LLM.

    Returns:
        ClassificationResult with content type and extracted fields.
    """
    # Step 1: Fast keyword classification
    result = classify_by_keywords(text)
    result.source_file = filepath or ""

    # Step 1.5: Check extraction templates (between keyword and LLM)
    if result.confidence < llm_threshold:
        try:
            from familiar.skills.intake.templates import get_template_store

            store = get_template_store()
            match = store.match_templates(text)
            if match:
                tmpl_type, tmpl_fields, tmpl_id = match
                try:
                    tmpl_ct = ContentType(tmpl_type)
                except ValueError:
                    tmpl_ct = None
                if tmpl_ct:
                    boosted = min(1.0, result.confidence + 0.2)
                    result = ClassificationResult(
                        content_type=tmpl_ct,
                        confidence=boosted,
                        extracted_text=text,
                        extracted_fields=tmpl_fields if "_matched" not in tmpl_fields else {},
                        source_file=filepath or "",
                        method="template",
                    )
        except Exception as e:
            logger.debug("Template matching failed: %s", e)

    # Step 2: If low confidence and LLM enabled, refine with LLM
    if use_llm and result.confidence < llm_threshold:
        llm_result = classify_with_llm(text, filepath=filepath)
        if llm_result and llm_result.confidence > result.confidence:
            result = llm_result

    # Flag for review if low confidence or unknown
    from familiar.skills.intake.review import REVIEW_CONFIDENCE_THRESHOLD

    if (
        result.confidence < REVIEW_CONFIDENCE_THRESHOLD
        or result.content_type == ContentType.UNKNOWN
    ):
        result.needs_review = True

    return result
