# 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

"""
Auto-routing engine for Smart Intake.

Maps classified content types to existing stores/skills and commits
extracted data. Routes work for ALL job classes — specialized stores
(donors, grants, events, advocacy) are tried first, with a universal
notes fallback that always works.
"""

import json as json_mod
import logging
import os
import secrets
import threading
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Callable, Optional

from familiar.skills.intake.classifiers import ClassificationResult, ContentType

logger = logging.getLogger(__name__)

_notes_lock = threading.Lock()


@dataclass
class RouteResult:
    """Result of routing classified content to a store."""

    success: bool
    destination: str  # e.g. "nonprofit.donors", "events", "notes"
    summary: str  # Human-readable summary of what was committed
    record_id: Optional[str] = None
    file_id: Optional[str] = None  # FileStore ID if original file was indexed
    memory_id: Optional[str] = None  # MemoryTree node ID


# ── Universal notes fallback ──────────────────────────────────────────
# Works for ANY job class — writes to intake_notes.json (not nonprofit.json)
# so it never depends on the nonprofit skill being loaded.


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

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


def _save_intake_note(content: str, category: str = "intake", fields: Optional[dict] = None) -> str:
    """Save a note to the universal intake notes store. Always succeeds."""
    notes_file = _get_notes_dir() / "intake_notes.json"
    note_id = f"inote_{secrets.token_hex(6)}"

    note = {
        "id": note_id,
        "content": content[:4000],
        "category": category,
        "fields": fields or {},
        "created_at": datetime.now().isoformat(),
    }

    with _notes_lock:
        notes_file.parent.mkdir(parents=True, exist_ok=True)
        try:
            data = json_mod.loads(notes_file.read_text(encoding="utf-8"))
        except Exception:
            data = {"notes": []}

        data["notes"].insert(0, note)
        data["notes"] = data["notes"][:500]  # Cap at 500

        tmp = notes_file.with_suffix(".tmp")
        tmp.write_text(json_mod.dumps(data, indent=2, default=str), encoding="utf-8")
        os.replace(str(tmp), str(notes_file))
        try:
            os.chmod(str(notes_file), 0o600)
        except OSError:
            pass

    return note_id


# ── Route handlers ────────────────────────────────────────────────────
# Each handler tries the specialized store first, falls back to universal notes.


def _route_donation_receipt(result: ClassificationResult) -> RouteResult:
    """Route donation receipt to nonprofit donor CRM, or notes as fallback."""
    fields = result.extracted_fields
    donor_name = fields.get("donor_name", "Unknown Donor")
    amount = fields.get("amount", "N/A")

    # Try specialized nonprofit store
    try:
        from familiar.skills.nonprofit.skill import log_donor

        donor_data = {
            "name": donor_name,
            "amount": amount,
            "type": "donation",
            "notes": f"Auto-imported from intake: {result.source_file}",
        }
        if fields.get("date"):
            donor_data["date"] = fields["date"]
        log_donor(donor_data)
        return RouteResult(
            success=True,
            destination="nonprofit.donors",
            summary=f"Logged donation: {donor_name} — {amount}",
        )
    except Exception:
        pass

    # Fallback: universal notes
    content = f"Donation Receipt\nDonor: {donor_name}\nAmount: {amount}"
    if fields.get("date"):
        content += f"\nDate: {fields['date']}"
    content += f"\nSource: {result.source_file}"
    _save_intake_note(content, category="donation", fields=fields)
    return RouteResult(
        success=True,
        destination="notes",
        summary=f"Saved donation receipt: {donor_name} — {amount}",
    )


def _route_grant_document(result: ClassificationResult) -> RouteResult:
    """Route grant document to grant pipeline, or notes as fallback."""
    fields = result.extracted_fields
    grant_name = fields.get("grant_name", "Imported Grant")
    funder = fields.get("funder", "Unknown")

    try:
        from familiar.skills.nonprofit.skill import add_grant

        add_grant(
            {
                "name": grant_name,
                "funder": funder,
                "amount": fields.get("amount", ""),
                "deadline": fields.get("deadline", ""),
                "notes": f"Auto-imported from intake: {result.source_file}",
            }
        )
        return RouteResult(
            success=True,
            destination="nonprofit.grants",
            summary=f"Added grant: {grant_name} from {funder}",
        )
    except Exception:
        pass

    content = f"Grant Document: {grant_name}\nFunder: {funder}"
    if fields.get("amount"):
        content += f"\nAmount: {fields['amount']}"
    if fields.get("deadline"):
        content += f"\nDeadline: {fields['deadline']}"
    _save_intake_note(content, category="grant", fields=fields)
    return RouteResult(
        success=True,
        destination="notes",
        summary=f"Saved grant doc: {grant_name} from {funder}",
    )


def _route_event_flyer(result: ClassificationResult) -> RouteResult:
    """Route event flyer to event management, or notes as fallback."""
    fields = result.extracted_fields
    event_name = fields.get("event_name", "Imported Event")

    try:
        from familiar.skills.events.skill import get_event_store

        store = get_event_store()
        eid = store.create_event(
            name=event_name,
            event_type=fields.get("event_type", "other"),
            start_date=fields.get("date", ""),
            venue=fields.get("venue", ""),
            notes=f"Auto-imported from intake: {result.source_file}",
        )
        return RouteResult(
            success=True,
            destination="events",
            summary=f"Created event: {event_name}",
            record_id=eid,
        )
    except Exception:
        pass

    content = f"Event: {event_name}"
    if fields.get("date"):
        content += f"\nDate: {fields['date']}"
    if fields.get("venue"):
        content += f"\nVenue: {fields['venue']}"
    _save_intake_note(content, category="event", fields=fields)
    return RouteResult(
        success=True,
        destination="notes",
        summary=f"Saved event info: {event_name}",
    )


def _route_legislation(result: ClassificationResult) -> RouteResult:
    """Route legislation to advocacy bill tracker, or notes as fallback."""
    fields = result.extracted_fields
    bill_number = fields.get("bill_number", "Unknown")
    title = fields.get("title", "Imported Bill")

    try:
        from familiar.skills.advocacy.skill import get_bill_store

        store = get_bill_store()
        bid = store.track_bill(
            bill_number=bill_number,
            title=title,
            jurisdiction=fields.get("jurisdiction", ""),
        )
        return RouteResult(
            success=True,
            destination="advocacy.bills",
            summary=f"Tracking bill: {bill_number} — {title}",
            record_id=bid,
        )
    except Exception:
        pass

    content = f"Legislation: {bill_number}\nTitle: {title}"
    if fields.get("jurisdiction"):
        content += f"\nJurisdiction: {fields['jurisdiction']}"
    _save_intake_note(content, category="legislation", fields=fields)
    return RouteResult(
        success=True,
        destination="notes",
        summary=f"Saved bill info: {bill_number} — {title}",
    )


def _route_invoice(result: ClassificationResult) -> RouteResult:
    """Route invoice to notes with structured fields."""
    fields = result.extracted_fields
    vendor = fields.get("vendor", "Unknown Vendor")
    amount = fields.get("amount", "N/A")
    content = (
        f"Invoice from {vendor}\n"
        f"Amount: {amount}\n"
        f"Due: {fields.get('due_date', 'N/A')}\n"
        f"Invoice #: {fields.get('invoice_number', 'N/A')}\n"
        f"Source: {result.source_file}"
    )

    # Try nonprofit notes first
    try:
        from familiar.skills.nonprofit.skill import add_note

        add_note({"content": content, "category": "invoice"})
        return RouteResult(
            success=True,
            destination="notes",
            summary=f"Logged invoice: {vendor} — {amount}",
        )
    except Exception:
        pass

    _save_intake_note(content, category="invoice", fields=fields)
    return RouteResult(
        success=True,
        destination="notes",
        summary=f"Saved invoice: {vendor} — {amount}",
    )


def _route_meeting_notes(result: ClassificationResult) -> RouteResult:
    """Route meeting notes to notes store."""
    fields = result.extracted_fields
    content = result.extracted_text[:2000]
    if fields.get("date"):
        content = f"Meeting ({fields['date']})\n\n{content}"

    try:
        from familiar.skills.nonprofit.skill import add_note

        add_note({"content": content, "category": "meeting"})
        return RouteResult(
            success=True,
            destination="notes",
            summary=f"Saved meeting notes ({len(result.extracted_text)} chars)",
        )
    except Exception:
        pass

    _save_intake_note(content, category="meeting", fields=fields)
    return RouteResult(
        success=True,
        destination="notes",
        summary=f"Saved meeting notes ({len(result.extracted_text)} chars)",
    )


def _route_business_card(result: ClassificationResult) -> RouteResult:
    """Route business card to contacts/donors, or notes as fallback."""
    fields = result.extracted_fields
    name = fields.get("name", "Unknown Contact")
    org = fields.get("organization", "")

    # Try nonprofit donor CRM
    try:
        from familiar.skills.nonprofit.skill import log_donor

        notes_parts = []
        if fields.get("title"):
            notes_parts.append(f"Title: {fields['title']}")
        if fields.get("email"):
            notes_parts.append(f"Email: {fields['email']}")
        if fields.get("phone"):
            notes_parts.append(f"Phone: {fields['phone']}")
        notes_parts.append(f"Auto-imported from intake: {result.source_file}")

        log_donor(
            {
                "name": name,
                "organization": org,
                "type": "contact",
                "notes": "; ".join(notes_parts),
            }
        )
        return RouteResult(
            success=True,
            destination="nonprofit.donors",
            summary=f"Added contact: {name}" + (f" ({org})" if org else ""),
        )
    except Exception:
        pass

    # Fallback: universal notes
    content = f"Contact: {name}"
    if org:
        content += f"\nOrganization: {org}"
    for key in ("title", "email", "phone"):
        if fields.get(key):
            content += f"\n{key.title()}: {fields[key]}"
    _save_intake_note(content, category="contact", fields=fields)
    return RouteResult(
        success=True,
        destination="notes",
        summary=f"Saved contact: {name}" + (f" ({org})" if org else ""),
    )


def _route_recipe(result: ClassificationResult) -> RouteResult:
    """Route recipe to notes."""
    fields = result.extracted_fields
    name = fields.get("name", "Imported Recipe")
    content = f"Recipe: {name}\n\n{result.extracted_text[:2000]}"

    try:
        from familiar.skills.nonprofit.skill import add_note

        add_note({"content": content, "category": "recipe"})
        return RouteResult(
            success=True,
            destination="notes",
            summary=f"Saved recipe: {name}",
        )
    except Exception:
        pass

    _save_intake_note(content, category="recipe", fields=fields)
    return RouteResult(success=True, destination="notes", summary=f"Saved recipe: {name}")


def _route_general(result: ClassificationResult) -> RouteResult:
    """Fallback: save as a general note. Always succeeds."""
    content = result.extracted_text[:2000]
    label = result.content_type.value.replace("_", " ").title()

    # Try nonprofit notes first
    try:
        from familiar.skills.nonprofit.skill import add_note

        add_note(
            {
                "content": f"[{label}] {content}",
                "category": "intake",
            }
        )
        return RouteResult(
            success=True,
            destination="notes",
            summary=f"Saved as note ({label}, {len(result.extracted_text)} chars)",
        )
    except Exception:
        pass

    # Universal fallback — always works, no external deps
    _save_intake_note(
        f"[{label}] {content}",
        category="intake",
        fields=result.extracted_fields,
    )
    return RouteResult(
        success=True,
        destination="notes",
        summary=f"Saved as note ({label}, {len(result.extracted_text)} chars)",
    )


# ── Route table ───────────────────────────────────────────────────────

ROUTE_TABLE: dict[ContentType, Callable[[ClassificationResult], RouteResult]] = {
    ContentType.DONATION_RECEIPT: _route_donation_receipt,
    ContentType.GRANT_DOCUMENT: _route_grant_document,
    ContentType.EVENT_FLYER: _route_event_flyer,
    ContentType.LEGISLATION: _route_legislation,
    ContentType.INVOICE: _route_invoice,
    ContentType.MEETING_NOTES: _route_meeting_notes,
    ContentType.BUSINESS_CARD: _route_business_card,
    ContentType.RECIPE: _route_recipe,
    ContentType.LETTER: _route_general,
    ContentType.SPREADSHEET_DATA: _route_general,
    ContentType.MEDICAL_RECORD: _route_general,
    ContentType.CASE_NOTE: _route_general,
    ContentType.INVENTORY_LIST: _route_general,
    ContentType.GENERAL_DOCUMENT: _route_general,
    ContentType.PHOTO: _route_general,
    ContentType.AUDIO_TRANSCRIPT: _route_general,
    ContentType.UNKNOWN: _route_general,
}


def _get_active_job_key() -> str:
    """Get the active job class key, or 'general' if none set."""
    try:
        from familiar.jobs import get_active_job_class

        jc = get_active_job_class()
        return jc.key if jc else "general"
    except Exception:
        return "general"


def _index_in_filestore(
    result: ClassificationResult,
    route_result: RouteResult,
) -> tuple[Optional[str], Optional[str]]:
    """
    Store the original file in FileStore and create a MemoryTree node
    linking the extracted text, classification, and route destination.

    Returns (file_id, memory_id) — both None if indexing fails gracefully.
    """
    source = result.source_file
    if not source:
        return None, None

    source_path = Path(source)
    if not source_path.exists():
        return None, None

    job_key = _get_active_job_key()
    content_type = result.content_type.value
    file_id = None
    memory_id = None

    # Step 1: Store original file in FileStore
    try:
        from familiar.core.files import get_file_store

        store = get_file_store()
        file_data = source_path.read_bytes()

        # Build a description from classification
        desc_parts = [content_type.replace("_", " ").title()]
        if result.confidence:
            desc_parts.append(f"{result.confidence:.0%} confidence")
        if route_result.destination:
            desc_parts.append(f"→ {route_result.destination}")

        record = store.store(
            data=file_data,
            filename=source_path.name,
            job_class=job_key,
            record_type="intake",
            record_id=route_result.record_id or f"intake_{secrets.token_hex(4)}",
            description=" | ".join(desc_parts),
        )
        file_id = record.file_id
        logger.debug("Indexed intake file in FileStore: %s", file_id)
    except Exception as e:
        logger.debug("FileStore indexing skipped: %s", e)

    # Step 2: Create MemoryTree node with extracted text + classification
    try:
        from familiar.core.memory_tree import get_memory_tree

        tree = get_memory_tree()
        label = content_type.replace("_", " ").title()
        title = f"Intake: {label} — {source_path.name}"

        # Build searchable body from extracted text + fields
        body_parts = [result.extracted_text[:3000]]
        if result.extracted_fields:
            body_parts.append("\n--- Extracted Fields ---")
            for k, v in result.extracted_fields.items():
                body_parts.append(f"{k}: {v}")

        tags = ["intake", content_type, job_key]
        meta = {
            "source": "intake",
            "content_type": content_type,
            "confidence": str(result.confidence),
            "method": result.method,
            "destination": route_result.destination,
            "source_file": source,
        }
        if file_id:
            meta["file_id"] = file_id
        if route_result.record_id:
            meta["record_id"] = route_result.record_id

        entry = tree.store(
            title=title,
            body="\n".join(body_parts),
            tier="long",
            tags=tags,
            meta=meta,
            source=f"intake:{source_path.name}",
            confidence=result.confidence,
            importance=0.6,
        )
        memory_id = entry.memory_id
        logger.debug("Indexed intake in MemoryTree: %s", memory_id)

        # Link FileStore record to MemoryTree node
        if file_id:
            try:
                store = get_file_store()
                store.add_mention(
                    file_id=file_id,
                    memory_id=memory_id,
                    context=f"Intake: {label}",
                )
            except Exception:
                pass

    except Exception as e:
        logger.debug("MemoryTree indexing skipped: %s", e)

    return file_id, memory_id


def route(result: ClassificationResult, dry_run: bool = False) -> RouteResult:
    """
    Route classified content to the appropriate store.

    Every route has a universal notes fallback — routing NEVER fails.
    After routing, indexes the original file in FileStore and creates
    a searchable MemoryTree node linking everything together.
    """
    handler = ROUTE_TABLE.get(result.content_type, _route_general)
    result.suggested_route = handler.__doc__.strip().split(".")[0] if handler.__doc__ else ""

    if dry_run:
        dest = result.content_type.value.replace("_", " ").title()
        return RouteResult(
            success=True,
            destination=handler.__name__.replace("_route_", ""),
            summary=f"Would route as {dest} → {handler.__name__}",
        )

    route_result = handler(result)

    # Index in FileStore + MemoryTree for unified search
    if route_result.success:
        file_id, memory_id = _index_in_filestore(result, route_result)
        route_result.file_id = file_id
        route_result.memory_id = memory_id

    return route_result
