# 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

"""
Marketplace Catalog — unified registry for all entity types.

Aggregates species, jobs, skills, tools, and bundles into a single
searchable, browsable catalog.  Auto-seeds from built-in registries.
"""

import json
import logging
import threading
from pathlib import Path
from typing import Optional

from .entities import (
    CatalogEntry,
    EntityType,
    JobEntry,
    SkillEntry,
    SpeciesEntry,
)

logger = logging.getLogger(__name__)


class MarketplaceCatalog:
    """Unified marketplace catalog for all entity types."""

    def __init__(self, auto_seed: bool = True):
        self._entries: dict[str, dict[str, CatalogEntry]] = {t.value: {} for t in EntityType}
        self._lock = threading.Lock()
        if auto_seed:
            self.seed()

    # ── CRUD ────────────────────────────────────────────────────────

    def add(self, entry: CatalogEntry) -> None:
        """Add or update a catalog entry."""
        with self._lock:
            self._entries[entry.entity_type.value][entry.id] = entry

    def remove(self, entity_type: str, entry_id: str) -> bool:
        """Remove an entry. Returns True if found and removed."""
        with self._lock:
            bucket = self._entries.get(entity_type, {})
            if entry_id in bucket:
                del bucket[entry_id]
                return True
            return False

    def get(self, entity_type: str, entry_id: str) -> Optional[CatalogEntry]:
        """Get a single entry by type and id."""
        with self._lock:
            return self._entries.get(entity_type, {}).get(entry_id)

    # ── Browse / Search ─────────────────────────────────────────────

    def browse(
        self,
        entity_type: Optional[str] = None,
        tags: Optional[list[str]] = None,
        source: Optional[str] = None,
        limit: int = 50,
        offset: int = 0,
    ) -> list[CatalogEntry]:
        """Browse catalog entries with optional filters."""
        with self._lock:
            results: list[CatalogEntry] = []
            buckets = (
                [self._entries[entity_type]]
                if entity_type and entity_type in self._entries
                else self._entries.values()
            )
            for bucket in buckets:
                for entry in bucket.values():
                    if source and entry.source != source:
                        continue
                    if tags and not any(t in entry.tags for t in tags):
                        continue
                    results.append(entry)

            results.sort(key=lambda e: (e.entity_type.value, e.id))
            return results[offset : offset + limit]

    def search(
        self,
        query: str,
        entity_type: Optional[str] = None,
    ) -> list[CatalogEntry]:
        """Full-text search across id/name/description/tags."""
        with self._lock:
            results: list[CatalogEntry] = []
            buckets = (
                [self._entries[entity_type]]
                if entity_type and entity_type in self._entries
                else self._entries.values()
            )
            for bucket in buckets:
                for entry in bucket.values():
                    if entry.matches(query):
                        results.append(entry)
            results.sort(key=lambda e: (e.entity_type.value, e.id))
            return results

    def stats(self) -> dict[str, int]:
        """Count of entries per entity type."""
        with self._lock:
            return {t: len(b) for t, b in self._entries.items()}

    def export_catalog(self) -> dict:
        """Full JSON-serializable export."""
        with self._lock:
            return {
                t: {eid: e.to_dict() for eid, e in bucket.items()}
                for t, bucket in self._entries.items()
            }

    # ── Seeding ─────────────────────────────────────────────────────

    def seed(self) -> dict[str, int]:
        """Auto-populate from built-in registries. Returns {type: count}."""
        counts: dict[str, int] = {}
        counts["species"] = self._seed_species()
        counts["job"] = self._seed_jobs()
        skill_count, tool_count = self._seed_skills()
        counts["skill"] = skill_count
        counts["tool"] = tool_count
        counts["familiar"] = self._load_custom_bundles()
        logger.debug("Marketplace catalog seeded: %s", counts)
        return counts

    def _seed_species(self) -> int:
        """Seed species from built-in registry."""
        from familiar.creature.species import (
            EVOLUTION_FLAVOR,
            SPECIES_INFO,
            SPECIES_VOICE_DIRECTIVES,
        )

        count = 0
        for key, info in SPECIES_INFO.items():
            species_entry = SpeciesEntry(
                key=info.key,
                label=info.label,
                description=info.description,
                personality=info.personality,
                baby_preview=list(info.baby_preview),
                voice_directive=SPECIES_VOICE_DIRECTIVES.get(key, ""),
                evolution_flavor=EVOLUTION_FLAVOR.get(key),
            )
            entry = CatalogEntry(
                id=key,
                entity_type=EntityType.SPECIES,
                name=info.label,
                version="1.0.0",
                author="Familiar",
                description=info.description,
                tags=["species", key],
                source="builtin",
                entity_data=_species_to_dict(species_entry),
            )
            self.add(entry)
            count += 1
        return count

    def _seed_jobs(self) -> int:
        """Seed jobs from the job class registry."""
        from familiar.jobs import JOB_CLASS_REGISTRY

        count = 0
        for key, jc in JOB_CLASS_REGISTRY.items():
            job_entry = JobEntry(
                key=jc.key,
                name=jc.name,
                icon=jc.icon,
                description=jc.description,
                prompt_fragment=jc.prompt_fragment,
                skill_weights=dict(jc.skill_weights),
                species_flavor=dict(jc.species_flavor),
                recommended_connects=list(jc.recommended_connects),
                proactive_focus=list(jc.proactive_focus),
            )
            entry = CatalogEntry(
                id=key,
                entity_type=EntityType.JOB,
                name=jc.name,
                version="1.0.0",
                author="Familiar",
                description=jc.description,
                tags=["job", key],
                source="builtin",
                entity_data=_job_to_dict(job_entry),
            )
            self.add(entry)
            count += 1
        return count

    def _seed_skills(self) -> tuple[int, int]:
        """Seed skills and tools from built-in skill directories."""
        from pathlib import Path as _Path

        skills_dir = _Path(__file__).resolve().parent.parent / "skills"
        skill_count = 0
        tool_count = 0

        if not skills_dir.exists():
            return skill_count, tool_count

        for item in sorted(skills_dir.iterdir()):
            if not item.is_dir() or item.name.startswith("_"):
                continue
            has_skill_md = (item / "SKILL.md").exists()
            has_skill_py = (item / "skill.py").exists()
            if not (has_skill_md or has_skill_py):
                continue

            skill_name = item.name
            description = ""
            if has_skill_md:
                try:
                    description = (item / "SKILL.md").read_text().split("\n", 3)
                    description = "\n".join(description[:3]).strip()
                except Exception:
                    pass

            tool_names: list[str] = []
            if has_skill_py:
                tool_names = _extract_tool_names(item / "skill.py")

            skill_entry = SkillEntry(
                key=skill_name,
                name=skill_name.replace("_", " ").title(),
                description=description if isinstance(description, str) else "",
                tool_names=tool_names,
                category=_guess_skill_category(skill_name),
            )
            entry = CatalogEntry(
                id=skill_name,
                entity_type=EntityType.SKILL,
                name=skill_entry.name,
                version="1.0.0",
                author="Familiar",
                description=skill_entry.description,
                tags=["skill", skill_name],
                source="builtin",
                entity_data={
                    "key": skill_entry.key,
                    "name": skill_entry.name,
                    "description": skill_entry.description,
                    "tool_names": skill_entry.tool_names,
                    "category": skill_entry.category,
                },
            )
            self.add(entry)
            skill_count += 1

            # Seed individual tools
            for tname in tool_names:
                tool_entry = CatalogEntry(
                    id=f"{skill_name}.{tname}",
                    entity_type=EntityType.TOOL,
                    name=tname,
                    version="1.0.0",
                    author="Familiar",
                    description=f"Tool from {skill_entry.name}",
                    tags=["tool", skill_name],
                    source="builtin",
                    entity_data={
                        "name": tname,
                        "skill": skill_name,
                        "category": skill_entry.category,
                    },
                )
                self.add(tool_entry)
                tool_count += 1

        return skill_count, tool_count

    def _load_custom_bundles(self) -> int:
        """Load custom Familiar bundles from disk."""
        from familiar.core.paths import DATA_DIR

        bundles_dir = DATA_DIR / "custom_bundles"
        if not bundles_dir.exists():
            return 0

        count = 0
        for f in sorted(bundles_dir.glob("*.json")):
            try:
                data = json.loads(f.read_text())
                meta = data.get("metadata", {})
                bundle = data.get("bundle", {})
                entry = CatalogEntry(
                    id=meta.get("id", f.stem),
                    entity_type=EntityType.FAMILIAR,
                    name=meta.get("name", f.stem),
                    version=meta.get("version", "0.0.0"),
                    author=meta.get("author", "custom"),
                    description=bundle.get("description", ""),
                    tags=["familiar", "bundle"],
                    source="custom",
                    entity_data=bundle,
                )
                self.add(entry)
                count += 1
            except Exception as e:
                logger.warning("Failed to load custom bundle %s: %s", f, e)

        return count


# ── Helpers ─────────────────────────────────────────────────────────


def _species_to_dict(s: SpeciesEntry) -> dict:
    """Convert SpeciesEntry to a plain dict."""
    d: dict = {
        "key": s.key,
        "label": s.label,
        "description": s.description,
        "personality": s.personality,
        "baby_preview": s.baby_preview,
        "voice_directive": s.voice_directive,
    }
    if s.evolution_flavor:
        d["evolution_flavor"] = s.evolution_flavor
    if s.sprites:
        d["sprites"] = s.sprites
    return d


def _job_to_dict(j: JobEntry) -> dict:
    """Convert JobEntry to a plain dict."""
    return {
        "key": j.key,
        "name": j.name,
        "icon": j.icon,
        "description": j.description,
        "prompt_fragment": j.prompt_fragment,
        "skill_weights": j.skill_weights,
        "species_flavor": j.species_flavor,
        "recommended_connects": j.recommended_connects,
        "proactive_focus": j.proactive_focus,
    }


def _extract_tool_names(skill_py: Path) -> list[str]:
    """Extract tool names from a skill.py's TOOLS list without importing it."""
    names: list[str] = []
    try:
        text = skill_py.read_text()
        import re

        for match in re.finditer(r'"name"\s*:\s*"([^"]+)"', text):
            names.append(match.group(1))
    except Exception:
        pass
    return names


def _guess_skill_category(skill_name: str) -> str:
    """Heuristic category assignment for built-in skills."""
    categories = {
        "communication": [
            "email",
            "messaging",
            "sms",
            "notifications",
            "contacts",
            "email_server",
            "proton",
        ],
        "productivity": [
            "tasks",
            "calendar",
            "planner",
            "todoist",
            "clockify",
            "calcom",
            "meetings",
            "jitsi",
        ],
        "search": [
            "websearch",
            "smart_search",
            "brave_search",
            "google_trends",
            "news",
        ],
        "knowledge": [
            "knowledge",
            "knowledge_base",
            "filereader",
            "memories",
        ],
        "finance": [
            "bookkeeping",
            "quickbooks",
            "stripe_pay",
            "square",
        ],
        "nonprofit": [
            "nonprofit",
            "globalgiving",
            "propublica_nonprofit",
            "grants_gov",
            "civicrm",
            "mailerlite",
            "kobo",
            "open_referral",
            "compliance",
        ],
        "food": [
            "fooddata",
            "openfoodfacts",
            "mealie",
            "spoonacular",
        ],
        "media": [
            "video",
            "voice",
            "transcription",
            "charts",
            "documents",
            "cloudinary_img",
            "jellyfin",
            "instagram",
        ],
        "home": ["homeassistant", "gpio", "pihole"],
        "security": ["encryption", "phi_detection", "vaultwarden", "audit"],
        "developer": ["gitea", "browser", "webapi", "reddit"],
        "commerce": ["etsy", "shippo", "hubspot"],
        "health": ["hrsa", "samhsa", "npi"],
        "system": [
            "rbac",
            "triggers",
            "workflows",
            "services",
            "setup",
            "marketplace",
            "sessions",
            "user_management",
            "reports",
            "training",
        ],
        "storage": ["gdrive", "joplin", "nextcloud"],
    }
    for category, names in categories.items():
        if skill_name in names:
            return category
    return "general"
