"""
Brynq Agent Groups — Team/Group Management for AI Agents (Phase 48).

Agents can form teams, departments, projects, and guilds. Each group gets
a private channel, can manage members with roles, create group-scoped tasks,
and track collective stats.

Storage:
  state/broker/groups/{group_id}.json  — group definition + membership
  state/broker/groups/activity.jsonl   — append-only activity log
"""

import json
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

from brynq.server import (
    BROKER_DIR,
    _write_json_sync,
    _read_jsonl_sync,
    _append_jsonl_sync,
)

# ── Paths ──────────────────────────────────────────────────────────────────

GROUPS_DIR = BROKER_DIR / "groups"
GROUPS_DIR.mkdir(parents=True, exist_ok=True)

ACTIVITY_LOG = GROUPS_DIR / "activity.jsonl"

CHANNELS_DIR = BROKER_DIR / "channels"
CHANNELS_DIR.mkdir(parents=True, exist_ok=True)

TASKS_DIR = BROKER_DIR / "tasks"
TASKS_DIR.mkdir(parents=True, exist_ok=True)

# ── Constants ──────────────────────────────────────────────────────────────

VALID_GROUP_TYPES = ("team", "department", "project", "guild")
VALID_ROLES = ("owner", "admin", "member")
ADMIN_ROLES = ("owner", "admin")


# ── Internal helpers ───────────────────────────────────────────────────────


def _load_group(group_id: str) -> Optional[dict]:
    """Load a group JSON file. Returns None if missing or corrupt."""
    path = GROUPS_DIR / f"{group_id}.json"
    if not path.exists():
        return None
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return None


def _save_group(group: dict) -> None:
    """Persist a group dict to disk."""
    path = GROUPS_DIR / f"{group['group_id']}.json"
    _write_json_sync(path, group)


def _log_activity(group_id: str, action: str, agent_id: str = "", details: str = "") -> None:
    """Append an entry to the group activity log."""
    entry = {
        "id": uuid.uuid4().hex[:12],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "group_id": group_id,
        "action": action,
        "agent_id": agent_id,
        "details": details,
    }
    _append_jsonl_sync(ACTIVITY_LOG, entry)


def _all_groups() -> list[dict]:
    """Load every group from disk."""
    groups = []
    if not GROUPS_DIR.exists():
        return groups
    for path in GROUPS_DIR.glob("*.json"):
        try:
            groups.append(json.loads(path.read_text("utf-8")))
        except (json.JSONDecodeError, OSError):
            continue
    return groups


def _channel_name(group_name: str) -> str:
    """Derive a channel name from a group name."""
    safe = group_name.lower().replace(" ", "-")
    # Strip anything that isn't alphanumeric or hyphen
    safe = "".join(c for c in safe if c.isalnum() or c == "-")
    return f"group-{safe}"


# ── 1. Create Group ───────────────────────────────────────────────────────


def create_group(
    name: str,
    description: str = "",
    group_type: str = "team",
    owner_id: Optional[str] = None,
    visibility: str = "public",
) -> dict:
    """Create a new agent group.

    Returns the full group dict including the generated group_id.
    """
    if not name or not name.strip():
        return {"error": "Group name is required"}

    if group_type not in VALID_GROUP_TYPES:
        return {"error": f"Invalid group_type. Must be one of: {', '.join(VALID_GROUP_TYPES)}"}

    if visibility not in ("public", "private"):
        return {"error": "visibility must be 'public' or 'private'"}

    group_id = uuid.uuid4().hex[:12]
    now = datetime.now(timezone.utc).isoformat()
    channel = _channel_name(name)

    members: dict[str, dict] = {}
    if owner_id:
        members[owner_id] = {
            "agent_id": owner_id,
            "role": "owner",
            "joined_at": now,
        }

    group = {
        "group_id": group_id,
        "name": name.strip(),
        "description": description,
        "group_type": group_type,
        "visibility": visibility,
        "channel": channel,
        "owner_id": owner_id,
        "members": members,
        "join_requests": [],
        "created_at": now,
        "updated_at": now,
    }

    _save_group(group)
    _log_activity(group_id, "group_created", agent_id=owner_id or "", details=f"type={group_type}")
    return group


# ── 2. Membership ─────────────────────────────────────────────────────────


def add_member(group_id: str, agent_id: str, role: str = "member") -> dict:
    """Add a member to a group. Only owner/admin can add directly."""
    if role not in VALID_ROLES:
        return {"error": f"Invalid role. Must be one of: {', '.join(VALID_ROLES)}"}

    group = _load_group(group_id)
    if not group:
        return {"error": "Group not found"}

    if agent_id in group["members"]:
        return {"error": "Agent is already a member"}

    now = datetime.now(timezone.utc).isoformat()
    group["members"][agent_id] = {
        "agent_id": agent_id,
        "role": role,
        "joined_at": now,
    }
    group["updated_at"] = now
    _save_group(group)
    _log_activity(group_id, "member_added", agent_id=agent_id, details=f"role={role}")
    return {"status": "added", "agent_id": agent_id, "role": role}


def remove_member(group_id: str, agent_id: str) -> dict:
    """Remove a member from a group."""
    group = _load_group(group_id)
    if not group:
        return {"error": "Group not found"}

    if agent_id not in group["members"]:
        return {"error": "Agent is not a member"}

    del group["members"][agent_id]
    group["updated_at"] = datetime.now(timezone.utc).isoformat()
    _save_group(group)
    _log_activity(group_id, "member_removed", agent_id=agent_id)
    return {"status": "removed", "agent_id": agent_id}


def list_members(group_id: str) -> list[dict] | dict:
    """List all members of a group with their roles."""
    group = _load_group(group_id)
    if not group:
        return {"error": "Group not found"}
    return list(group["members"].values())


def get_member_groups(agent_id: str) -> list[dict]:
    """Return all groups that an agent belongs to."""
    result = []
    for group in _all_groups():
        if agent_id in group.get("members", {}):
            result.append({
                "group_id": group["group_id"],
                "name": group["name"],
                "group_type": group["group_type"],
                "role": group["members"][agent_id]["role"],
            })
    return result


# ── 3. Group Channel ──────────────────────────────────────────────────────


def get_group_channel(group_id: str) -> dict:
    """Return the private channel name for a group."""
    group = _load_group(group_id)
    if not group:
        return {"error": "Group not found"}
    return {"channel": group["channel"], "group_id": group_id, "name": group["name"]}


def post_to_group_channel(group_id: str, agent_id: str, message: str) -> dict:
    """Post a message to the group's private channel. Only members can post."""
    group = _load_group(group_id)
    if not group:
        return {"error": "Group not found"}

    if agent_id not in group["members"]:
        return {"error": "Only group members can post to the group channel"}

    if not message or not message.strip():
        return {"error": "Message cannot be empty"}

    channel = group["channel"]
    entry = {
        "id": uuid.uuid4().hex[:12],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "agent_id": agent_id,
        "group_id": group_id,
        "channel": channel,
        "message": message.strip(),
    }
    channel_path = CHANNELS_DIR / f"{channel}.jsonl"
    _append_jsonl_sync(channel_path, entry)
    return {"status": "posted", "channel": channel, "message_id": entry["id"]}


def read_group_channel(group_id: str, agent_id: str, offset: int = 0) -> list[dict] | dict:
    """Read messages from a group's private channel. Only members can read."""
    group = _load_group(group_id)
    if not group:
        return {"error": "Group not found"}

    if agent_id not in group["members"]:
        return {"error": "Only group members can read the group channel"}

    channel = group["channel"]
    channel_path = CHANNELS_DIR / f"{channel}.jsonl"
    return _read_jsonl_sync(channel_path, offset)


# ── 4. Group Tasks ────────────────────────────────────────────────────────


def create_group_task(
    group_id: str,
    title: str,
    description: str = "",
    required_skills: Optional[list[str]] = None,
    creator_id: Optional[str] = None,
) -> dict:
    """Create a task scoped to a group — only members can see/claim it."""
    group = _load_group(group_id)
    if not group:
        return {"error": "Group not found"}

    if not title or not title.strip():
        return {"error": "Task title is required"}

    task_id = uuid.uuid4().hex[:12]
    now = datetime.now(timezone.utc).isoformat()

    task = {
        "task_id": task_id,
        "group_id": group_id,
        "title": title.strip(),
        "description": description,
        "required_skills": required_skills or [],
        "status": "open",
        "creator_id": creator_id,
        "assignee_id": None,
        "created_at": now,
        "updated_at": now,
        "completed_at": None,
    }

    task_path = TASKS_DIR / f"{task_id}.json"
    _write_json_sync(task_path, task)

    _log_activity(group_id, "task_created", agent_id=creator_id or "", details=title)
    return task


def list_group_tasks(group_id: str, status: Optional[str] = None) -> list[dict] | dict:
    """List all tasks belonging to a group, optionally filtered by status."""
    group = _load_group(group_id)
    if not group:
        return {"error": "Group not found"}

    tasks = []
    if not TASKS_DIR.exists():
        return tasks
    for path in TASKS_DIR.glob("*.json"):
        try:
            task = json.loads(path.read_text("utf-8"))
            if task.get("group_id") == group_id:
                if status is None or task.get("status") == status:
                    tasks.append(task)
        except (json.JSONDecodeError, OSError):
            continue

    # Sort by creation time descending
    tasks.sort(key=lambda t: t.get("created_at", ""), reverse=True)
    return tasks


def complete_group_task(task_id: str, agent_id: str) -> dict:
    """Mark a group task as completed."""
    task_path = TASKS_DIR / f"{task_id}.json"
    if not task_path.exists():
        return {"error": "Task not found"}

    try:
        task = json.loads(task_path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return {"error": "Task not found"}

    if task.get("status") == "completed":
        return {"error": "Task is already completed"}

    now = datetime.now(timezone.utc).isoformat()
    task["status"] = "completed"
    task["assignee_id"] = agent_id
    task["completed_at"] = now
    task["updated_at"] = now
    _write_json_sync(task_path, task)

    group_id = task.get("group_id", "")
    _log_activity(group_id, "task_completed", agent_id=agent_id, details=task["title"])
    return task


# ── 5. Group Stats ────────────────────────────────────────────────────────


def get_group_stats(group_id: str) -> dict:
    """Return aggregate stats for a group."""
    group = _load_group(group_id)
    if not group:
        return {"error": "Group not found"}

    members = group.get("members", {})
    member_count = len(members)

    # Count tasks
    all_tasks = list_group_tasks(group_id)
    if isinstance(all_tasks, dict):  # error
        all_tasks = []

    completed_tasks = [t for t in all_tasks if t.get("status") == "completed"]
    active_tasks = [t for t in all_tasks if t.get("status") == "open"]

    # Top contributors (by completed tasks)
    contributor_counts: dict[str, int] = {}
    for t in completed_tasks:
        aid = t.get("assignee_id")
        if aid:
            contributor_counts[aid] = contributor_counts.get(aid, 0) + 1
    top_contributors = sorted(contributor_counts.items(), key=lambda x: x[1], reverse=True)[:5]

    # Group reputation: load profiles and average reputation scores
    profiles_dir = BROKER_DIR / "profiles"
    total_rep = 0
    rep_count = 0
    for agent_id in members:
        profile_path = profiles_dir / f"{agent_id}.json"
        if profile_path.exists():
            try:
                profile = json.loads(profile_path.read_text("utf-8"))
                rep = profile.get("reputation", profile.get("reputation_score", 0))
                if isinstance(rep, (int, float)):
                    total_rep += rep
                    rep_count += 1
            except (json.JSONDecodeError, OSError):
                continue

    avg_reputation = round(total_rep / rep_count, 2) if rep_count > 0 else 0.0

    return {
        "group_id": group_id,
        "name": group["name"],
        "member_count": member_count,
        "total_tasks": len(all_tasks),
        "completed_tasks": len(completed_tasks),
        "active_tasks": len(active_tasks),
        "top_contributors": [{"agent_id": a, "tasks_completed": c} for a, c in top_contributors],
        "group_reputation": avg_reputation,
    }


# ── 6. Group Discovery ───────────────────────────────────────────────────


def list_groups(group_type: Optional[str] = None, include_private: bool = False) -> list[dict]:
    """List all discoverable groups, optionally filtered by type.

    By default only public groups are returned. Pass include_private=True
    to also include private groups.
    """
    groups = _all_groups()
    results = []
    for g in groups:
        if not include_private and g.get("visibility") == "private":
            continue
        if group_type and g.get("group_type") != group_type:
            continue
        results.append({
            "group_id": g["group_id"],
            "name": g["name"],
            "description": g.get("description", ""),
            "group_type": g["group_type"],
            "visibility": g.get("visibility", "public"),
            "member_count": len(g.get("members", {})),
            "created_at": g.get("created_at", ""),
        })
    results.sort(key=lambda x: x["created_at"], reverse=True)
    return results


def search_groups(query: str) -> list[dict]:
    """Search groups by name or description. Only public groups are returned."""
    if not query or not query.strip():
        return []
    query_lower = query.lower().strip()
    groups = _all_groups()
    results = []
    for g in groups:
        if g.get("visibility") == "private":
            continue
        name_match = query_lower in g.get("name", "").lower()
        desc_match = query_lower in g.get("description", "").lower()
        type_match = query_lower in g.get("group_type", "").lower()
        if name_match or desc_match or type_match:
            results.append({
                "group_id": g["group_id"],
                "name": g["name"],
                "description": g.get("description", ""),
                "group_type": g["group_type"],
                "member_count": len(g.get("members", {})),
            })
    return results


def get_group(group_id: str) -> Optional[dict]:
    """Get full group details by ID."""
    return _load_group(group_id)


# ── 7. Join / Leave ──────────────────────────────────────────────────────


def join_group(group_id: str, agent_id: str) -> dict:
    """Join a public group directly."""
    group = _load_group(group_id)
    if not group:
        return {"error": "Group not found"}

    if group.get("visibility") == "private":
        return {"error": "This is a private group. Use request_join instead."}

    if agent_id in group["members"]:
        return {"error": "Already a member"}

    now = datetime.now(timezone.utc).isoformat()
    group["members"][agent_id] = {
        "agent_id": agent_id,
        "role": "member",
        "joined_at": now,
    }
    group["updated_at"] = now
    _save_group(group)
    _log_activity(group_id, "member_joined", agent_id=agent_id)
    return {"status": "joined", "group_id": group_id, "role": "member"}


def request_join(group_id: str, agent_id: str, message: str = "") -> dict:
    """Request to join a private group. Requires admin approval."""
    group = _load_group(group_id)
    if not group:
        return {"error": "Group not found"}

    if agent_id in group["members"]:
        return {"error": "Already a member"}

    # Check for existing pending request
    for req in group.get("join_requests", []):
        if req["agent_id"] == agent_id and req["status"] == "pending":
            return {"error": "Join request already pending"}

    now = datetime.now(timezone.utc).isoformat()
    request_entry = {
        "request_id": uuid.uuid4().hex[:12],
        "agent_id": agent_id,
        "message": message,
        "status": "pending",
        "requested_at": now,
    }

    if "join_requests" not in group:
        group["join_requests"] = []
    group["join_requests"].append(request_entry)
    group["updated_at"] = now
    _save_group(group)
    _log_activity(group_id, "join_requested", agent_id=agent_id)
    return {"status": "requested", "request_id": request_entry["request_id"]}


def approve_join(group_id: str, agent_id: str, approver_id: str) -> dict:
    """Approve a pending join request. Approver must be owner or admin."""
    group = _load_group(group_id)
    if not group:
        return {"error": "Group not found"}

    # Verify approver is admin/owner
    approver = group["members"].get(approver_id)
    if not approver or approver["role"] not in ADMIN_ROLES:
        return {"error": "Only owners and admins can approve join requests"}

    # Find the pending request
    found = False
    for req in group.get("join_requests", []):
        if req["agent_id"] == agent_id and req["status"] == "pending":
            req["status"] = "approved"
            req["approved_by"] = approver_id
            req["approved_at"] = datetime.now(timezone.utc).isoformat()
            found = True
            break

    if not found:
        return {"error": "No pending join request from this agent"}

    # Add as member
    now = datetime.now(timezone.utc).isoformat()
    group["members"][agent_id] = {
        "agent_id": agent_id,
        "role": "member",
        "joined_at": now,
    }
    group["updated_at"] = now
    _save_group(group)
    _log_activity(group_id, "join_approved", agent_id=agent_id, details=f"approved_by={approver_id}")
    return {"status": "approved", "agent_id": agent_id}


def leave_group(group_id: str, agent_id: str) -> dict:
    """Leave a group voluntarily."""
    group = _load_group(group_id)
    if not group:
        return {"error": "Group not found"}

    if agent_id not in group["members"]:
        return {"error": "Not a member of this group"}

    del group["members"][agent_id]
    group["updated_at"] = datetime.now(timezone.utc).isoformat()
    _save_group(group)
    _log_activity(group_id, "member_left", agent_id=agent_id)
    return {"status": "left", "group_id": group_id}
