# 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

"""
Batch Communications & Mail Merge — segmented donor outreach.

Supports annual appeals, LYBUNT reactivation, major donor updates,
and first-time donor welcome sequences. Generates individual emails
or documents per recipient with template merge fields.

Stored at: ~/.familiar/data/batch_comms.json
"""

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

logger = logging.getLogger(__name__)

_lock = threading.Lock()


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

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


# ── Predefined Segments ──────────────────────────────────────────────

PREDEFINED_SEGMENTS = {
    "annual_appeal": {
        "name": "Annual Appeal — All Active Donors",
        "criteria": {"active": True},
    },
    "lybunt_reactivation": {
        "name": "LYBUNT Reactivation",
        "criteria": {"lybunt": True},
    },
    "major_donor_update": {
        "name": "Major Donor Update",
        "criteria": {"level": "major"},
    },
    "first_time_welcome": {
        "name": "First-Time Donor Welcome",
        "criteria": {"gift_count_max": 1},
    },
}

BATCH_STATUSES = ["draft", "ready", "generating", "generated", "sending", "sent", "cancelled"]


# ── Donor Segmentation ───────────────────────────────────────────────


def segment_donors(criteria: dict) -> list[dict]:
    """Filter donors by criteria. Returns matching donor records."""
    try:
        from familiar.skills.nonprofit.skill import get_all_donors

        donors = get_all_donors()
    except Exception:
        return []

    results = []
    for d in donors:
        if not _matches_criteria(d, criteria):
            continue
        results.append(d)
    return results


def _matches_criteria(donor: dict, criteria: dict) -> bool:
    """Check if a donor matches the given criteria."""
    level = criteria.get("level")
    if level and donor.get("donor_level", "").lower() != level.lower():
        return False

    min_rfm = criteria.get("rfm_min")
    if min_rfm is not None and donor.get("rfm_score", 0) < min_rfm:
        return False

    max_rfm = criteria.get("rfm_max")
    if max_rfm is not None and donor.get("rfm_score", 0) > max_rfm:
        return False

    lybunt = criteria.get("lybunt")
    if lybunt and not donor.get("lybunt"):
        return False

    sybunt = criteria.get("sybunt")
    if sybunt and not donor.get("sybunt"):
        return False

    min_total = criteria.get("min_total")
    if min_total is not None and donor.get("total_given", 0) < min_total:
        return False

    max_total = criteria.get("max_total")
    if max_total is not None and donor.get("total_given", 0) > max_total:
        return False

    gift_count_max = criteria.get("gift_count_max")
    if gift_count_max is not None:
        count = len(donor.get("gifts", donor.get("interactions", [])))
        if count > gift_count_max:
            return False

    active = criteria.get("active")
    if active is True and donor.get("status", "active") != "active":
        return False

    tags = criteria.get("tags")
    if tags:
        donor_tags = set(t.lower() for t in donor.get("tags", []))
        if not any(t.lower() in donor_tags for t in tags):
            return False

    return True


# ── BatchStore ────────────────────────────────────────────────────────


class BatchStore:
    """Thread-safe store for batch communication jobs."""

    def __init__(self):
        self._path = _data_dir() / "batch_comms.json"
        self._data: dict = {"batches": []}
        self.load()

    def load(self):
        with _lock:
            if self._path.exists():
                try:
                    self._data = json.loads(self._path.read_text())
                except (json.JSONDecodeError, OSError) as e:
                    logger.warning("Failed to load batch comms: %s", e)
                    self._data = {"batches": []}
            else:
                self._data = {"batches": []}

    def save(self):
        with _lock:
            self._path.parent.mkdir(parents=True, exist_ok=True)
            tmp = self._path.with_suffix(".tmp")
            tmp.write_text(json.dumps(self._data, indent=2))
            tmp.replace(self._path)
            try:
                os.chmod(self._path, 0o600)
            except OSError:
                pass

    def create_batch(
        self,
        name: str,
        segment_key: str = "",
        segment_criteria: Optional[dict] = None,
        template_name: str = "",
        channel: str = "email",
    ) -> str:
        """Create a new batch communication job. Returns batch_id."""
        batch_id = f"batch_{secrets.token_hex(6)}"
        now = datetime.now(timezone.utc).isoformat()

        # Resolve predefined segment
        if segment_key and segment_key in PREDEFINED_SEGMENTS:
            criteria = PREDEFINED_SEGMENTS[segment_key]["criteria"]
            if not name:
                name = PREDEFINED_SEGMENTS[segment_key]["name"]
        else:
            criteria = segment_criteria or {}

        # Segment donors
        recipients = segment_donors(criteria)
        recipient_ids = [d.get("id", d.get("name", "")) for d in recipients]

        self._data["batches"].append(
            {
                "id": batch_id,
                "name": name,
                "status": "draft",
                "segment_key": segment_key,
                "segment_criteria": criteria,
                "template_name": template_name,
                "channel": channel,
                "recipients": recipient_ids,
                "recipients_count": len(recipient_ids),
                "generated_count": 0,
                "sent_count": 0,
                "failed_count": 0,
                "items": [],
                "created_at": now,
                "updated_at": now,
            }
        )
        self.save()
        return batch_id

    def get_batch(self, batch_id: str) -> Optional[dict]:
        self.load()
        for b in self._data["batches"]:
            if b["id"] == batch_id:
                return b
        return None

    def list_batches(self) -> list[dict]:
        self.load()
        return self._data["batches"]

    def update_status(self, batch_id: str, status: str) -> bool:
        for b in self._data["batches"]:
            if b["id"] == batch_id:
                b["status"] = status
                b["updated_at"] = datetime.now(timezone.utc).isoformat()
                self.save()
                return True
        return False

    def record_generation(self, batch_id: str, donor_id: str, content: str) -> bool:
        """Record that a communication was generated for a donor."""
        for b in self._data["batches"]:
            if b["id"] == batch_id:
                b["items"].append(
                    {
                        "donor_id": donor_id,
                        "status": "generated",
                        "content": content[:500],  # Truncate for storage
                        "generated_at": datetime.now(timezone.utc).isoformat(),
                        "sent_at": None,
                        "error": None,
                    }
                )
                b["generated_count"] = len(b["items"])
                self.save()
                return True
        return False

    def record_send(self, batch_id: str, donor_id: str, success: bool, error: str = "") -> bool:
        """Record send result for a donor in a batch."""
        for b in self._data["batches"]:
            if b["id"] == batch_id:
                for item in b["items"]:
                    if item["donor_id"] == donor_id:
                        if success:
                            item["status"] = "sent"
                            item["sent_at"] = datetime.now(timezone.utc).isoformat()
                            b["sent_count"] = sum(1 for i in b["items"] if i["status"] == "sent")
                        else:
                            item["status"] = "failed"
                            item["error"] = error
                            b["failed_count"] = sum(
                                1 for i in b["items"] if i["status"] == "failed"
                            )
                        self.save()
                        return True
        return False

    def cancel_batch(self, batch_id: str) -> bool:
        return self.update_status(batch_id, "cancelled")


def get_batch_store() -> BatchStore:
    return BatchStore()
