# 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

"""
Co-op Data Pooling

Enables multiple Familiar instances to pool ethically-sourced training data
into cooperative datasets. Each member contributes curated records with
verified consent and quality attestation.

Features:
  - Create and join co-ops with configurable rules
  - Contribute curated data with consent verification
  - Merge contributions from multiple members
  - Export cooperative datasets with full provenance

Storage: ~/.familiar/data/coop/ (tenant-scoped)

Dependencies: None (pure stdlib)
"""

import hashlib
import json
import logging
from dataclasses import asdict, dataclass, field
from datetime import datetime
from pathlib import Path

logger = logging.getLogger(__name__)


def _get_coop_dir():
    """Get co-op data directory (tenant-scoped)."""
    try:
        from familiar.core.paths import COOP_DIR

        COOP_DIR.mkdir(parents=True, exist_ok=True)
        return COOP_DIR
    except ImportError:
        d = Path.home() / ".familiar" / "data" / "coop"
        d.mkdir(parents=True, exist_ok=True)
        return d


@dataclass
class CoopMember:
    """A member of a training data co-op."""

    genesis_hash: str
    public_key_pem: str = ""
    joined_at: str = ""
    display_name: str = ""


@dataclass
class CoopRules:
    """Rules governing a co-op's data quality and consent requirements."""

    min_quality: float = 0.5
    required_consent: list = field(default_factory=lambda: ["coop_sharing"])
    max_records_per_member: int = 10000
    allowed_sources: list = field(
        default_factory=lambda: ["conversations", "tool_calls", "generated", "memories"]
    )


@dataclass
class CoopManifest:
    """Manifest describing a co-op: members, rules, stats."""

    coop_id: str
    name: str
    description: str = ""
    created_at: str = ""
    members: list = field(default_factory=list)
    rules: dict = field(default_factory=dict)
    contribution_stats: dict = field(default_factory=dict)


class CoopManager:
    """
    Manages cooperative training data pooling.

    Coordinates creation, membership, contribution, and merging of
    co-op datasets with full provenance and consent verification.
    """

    def __init__(self, data_dir: Path = None):
        self._coop_dir = data_dir or _get_coop_dir()
        self._coop_dir.mkdir(parents=True, exist_ok=True)

    def _get_own_genesis_hash(self) -> str:
        """Get this instance's genesis block hash."""
        try:
            from familiar.skills.training.bridge import get_chain_status

            status = get_chain_status()
            return status.get("genesis_hash", "") or ""
        except Exception:
            return ""

    def _get_own_public_key(self) -> str:
        """Get this instance's public key PEM."""
        try:
            from familiar.core.genesis import load_or_create_keypair
            from familiar.core.paths import CHAIN_DIR

            key_dir = CHAIN_DIR / "keys"
            _, pub = load_or_create_keypair(key_dir)
            from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

            return pub.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo).decode()
        except Exception:
            return ""

    def create_coop(
        self, name: str, description: str = "", rules: CoopRules = None
    ) -> CoopManifest:
        """
        Create a new co-op with this instance as the founding member.

        Args:
            name: Human-readable co-op name
            description: Purpose/description
            rules: Quality and consent rules (defaults applied if None)

        Returns:
            CoopManifest for the new co-op
        """
        if rules is None:
            rules = CoopRules()

        coop_id = hashlib.sha256(f"{name}:{datetime.now().isoformat()}".encode()).hexdigest()[:16]

        now = datetime.now().isoformat()
        genesis_hash = self._get_own_genesis_hash()

        founder = CoopMember(
            genesis_hash=genesis_hash or coop_id,
            public_key_pem=self._get_own_public_key(),
            joined_at=now,
            display_name="founder",
        )

        manifest = CoopManifest(
            coop_id=coop_id,
            name=name,
            description=description,
            created_at=now,
            members=[asdict(founder)],
            rules=asdict(rules),
            contribution_stats={},
        )

        # Save manifest
        coop_path = self._coop_dir / coop_id
        coop_path.mkdir(parents=True, exist_ok=True)
        self._save_manifest(coop_id, manifest)

        logger.info("Created co-op %s: %s", coop_id, name)
        return manifest

    def join_coop(self, manifest_path: str) -> bool:
        """
        Join an existing co-op by importing its manifest.

        Args:
            manifest_path: Path to a co-op manifest JSON file

        Returns:
            True if successfully joined
        """
        path = Path(manifest_path)
        if not path.exists():
            raise FileNotFoundError(f"Manifest not found: {manifest_path}")

        with open(path, encoding="utf-8") as f:
            data = json.load(f)

        coop_id = data.get("coop_id")
        if not coop_id:
            raise ValueError("Invalid manifest: missing coop_id")

        # Check if already a member
        genesis_hash = self._get_own_genesis_hash()
        members = data.get("members", [])
        for m in members:
            if m.get("genesis_hash") == genesis_hash:
                logger.info("Already a member of co-op %s", coop_id)
                return True

        # Add self as member
        now = datetime.now().isoformat()
        new_member = CoopMember(
            genesis_hash=genesis_hash or hashlib.sha256(now.encode()).hexdigest()[:16],
            public_key_pem=self._get_own_public_key(),
            joined_at=now,
            display_name="member",
        )
        data["members"].append(asdict(new_member))

        # Save locally
        coop_path = self._coop_dir / coop_id
        coop_path.mkdir(parents=True, exist_ok=True)

        # Save the manifest dict directly
        with open(coop_path / "manifest.json", "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2, default=str)

        logger.info("Joined co-op %s", coop_id)
        return True

    def contribute(self, coop_id: str) -> dict:
        """
        Contribute local curated data to a co-op.

        Filters by co-op rules (quality, consent, sources) and writes
        contribution bundle to the co-op directory.

        Returns dict with contribution stats.
        """
        manifest = self._load_manifest(coop_id)
        if manifest is None:
            return {"error": f"Co-op {coop_id} not found"}

        rules = manifest.get("rules", {})
        min_quality = rules.get("min_quality", 0.5)
        allowed_sources = rules.get("allowed_sources", ["conversations", "tool_calls", "generated"])
        required_consent = rules.get("required_consent", ["coop_sharing"])
        max_records = rules.get("max_records_per_member", 10000)

        # Load and filter curated data
        try:
            from familiar.skills.training.skill import _get_curated_dir, _read_jsonl
        except ImportError:
            return {"error": "Training skill not available"}

        curated_dir = _get_curated_dir()
        records = []
        for src in allowed_sources:
            recs = _read_jsonl(curated_dir / f"{src}.jsonl")
            records.extend(recs)

        # Filter by quality
        records = [r for r in records if r.get("quality_score", 0) >= min_quality]

        # Filter by consent
        if required_consent:
            try:
                from familiar.core.consent import ConsentManager

                cm = ConsentManager()
                filtered = []
                for r in records:
                    author = r.get("provenance", {}).get("author", {})
                    uid = author.get("id", "") if isinstance(author, dict) else str(author)
                    if uid and all(cm.check_consent(uid, scope) for scope in required_consent):
                        filtered.append(r)
                records = filtered
            except ImportError:
                logger.debug("Consent module not available, skipping consent check")

        # Cap records
        records = records[:max_records]

        if not records:
            return {"records_contributed": 0, "message": "No records pass co-op rules"}

        # Write contribution
        genesis_hash = self._get_own_genesis_hash() or "local"
        coop_path = self._coop_dir / coop_id / "contributions"
        coop_path.mkdir(parents=True, exist_ok=True)

        contrib_file = coop_path / f"{genesis_hash}.jsonl"
        with open(contrib_file, "w", encoding="utf-8") as f:
            for r in records:
                f.write(json.dumps(r, default=str) + "\n")

        # Update stats
        stats = {
            "genesis_hash": genesis_hash,
            "records_contributed": len(records),
            "avg_quality": round(sum(r.get("quality_score", 0) for r in records) / len(records), 3),
            "contributed_at": datetime.now().isoformat(),
        }

        manifest_data = self._load_manifest(coop_id)
        if manifest_data:
            cs = manifest_data.get("contribution_stats", {})
            cs[genesis_hash] = stats
            manifest_data["contribution_stats"] = cs
            with open(self._coop_dir / coop_id / "manifest.json", "w", encoding="utf-8") as f:
                json.dump(manifest_data, f, indent=2, default=str)

        return stats

    def merge_contributions(self, coop_id: str) -> dict:
        """
        Merge all contributions into a unified co-op dataset.

        Deduplicates by record ID and writes merged JSONL.
        Returns merge stats.
        """
        contrib_dir = self._coop_dir / coop_id / "contributions"
        if not contrib_dir.exists():
            return {"error": "No contributions found"}

        seen_ids = set()
        merged = []

        for contrib_file in sorted(contrib_dir.glob("*.jsonl")):
            with open(contrib_file, encoding="utf-8") as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        record = json.loads(line)
                        rid = record.get("id", "")
                        if rid and rid not in seen_ids:
                            seen_ids.add(rid)
                            merged.append(record)
                    except json.JSONDecodeError:
                        continue

        if not merged:
            return {"merged_records": 0}

        # Write merged dataset
        merged_path = self._coop_dir / coop_id / "merged.jsonl"
        with open(merged_path, "w", encoding="utf-8") as f:
            for r in merged:
                f.write(json.dumps(r, default=str) + "\n")

        return {
            "merged_records": len(merged),
            "contributors": len(list(contrib_dir.glob("*.jsonl"))),
            "merged_path": str(merged_path),
        }

    def get_contribution_stats(self, coop_id: str) -> dict:
        """Get contribution statistics for a co-op."""
        manifest = self._load_manifest(coop_id)
        if manifest is None:
            return {"error": f"Co-op {coop_id} not found"}

        return {
            "coop_id": coop_id,
            "name": manifest.get("name", ""),
            "members": len(manifest.get("members", [])),
            "contribution_stats": manifest.get("contribution_stats", {}),
        }

    def export_coop_dataset(self, coop_id: str, fmt: str = "alpaca") -> str:
        """
        Export the merged co-op dataset in a standard format.

        Returns path to exported dataset.
        """
        merged_path = self._coop_dir / coop_id / "merged.jsonl"
        if not merged_path.exists():
            # Try merging first
            result = self.merge_contributions(coop_id)
            if result.get("merged_records", 0) == 0:
                return "No data to export. Contribute data first."

        records = []
        with open(merged_path, encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if line:
                    try:
                        records.append(json.loads(line))
                    except json.JSONDecodeError:
                        continue

        if not records:
            return "No records in merged dataset."

        # Format
        formatted = []
        for r in records:
            instruction = r.get("instruction", "")
            response = r.get("response", "")
            if fmt == "alpaca":
                formatted.append({"instruction": instruction, "input": "", "output": response})
            elif fmt == "sharegpt":
                formatted.append(
                    {
                        "conversations": [
                            {"from": "human", "value": instruction},
                            {"from": "gpt", "value": response},
                        ]
                    }
                )
            elif fmt == "openai":
                formatted.append(
                    {
                        "messages": [
                            {"role": "user", "content": instruction},
                            {"role": "assistant", "content": response},
                        ]
                    }
                )

        export_path = self._coop_dir / coop_id / f"coop_dataset_{fmt}.jsonl"
        with open(export_path, "w", encoding="utf-8") as f:
            for rec in formatted:
                f.write(json.dumps(rec) + "\n")

        return str(export_path)

    def list_coops(self) -> list:
        """List all co-ops this instance belongs to."""
        coops = []
        if not self._coop_dir.exists():
            return coops

        for d in sorted(self._coop_dir.iterdir()):
            if d.is_dir() and (d / "manifest.json").exists():
                manifest = self._load_manifest(d.name)
                if manifest:
                    coops.append(
                        {
                            "coop_id": d.name,
                            "name": manifest.get("name", ""),
                            "members": len(manifest.get("members", [])),
                            "created_at": manifest.get("created_at", ""),
                        }
                    )
        return coops

    def _save_manifest(self, coop_id: str, manifest: CoopManifest):
        """Save a co-op manifest to disk."""
        path = self._coop_dir / coop_id / "manifest.json"
        path.parent.mkdir(parents=True, exist_ok=True)
        with open(path, "w", encoding="utf-8") as f:
            json.dump(asdict(manifest), f, indent=2, default=str)

    def _load_manifest(self, coop_id: str) -> dict:
        """Load a co-op manifest from disk."""
        path = self._coop_dir / coop_id / "manifest.json"
        if not path.exists():
            return None
        try:
            with open(path, encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError):
            return None
