"""
Phase 25: Swarm Blueprints & Collective Reasoning (Brynq Exchange)

A Swarm Blueprint packages multiple agents and their required tasks into a single
deployable asset. Blueprints can be published to the Brynq Exchange, discovered,
and deployed with a single command. This enables "Collective Intelligence" by
codifying proven multi-agent reasoning structures.

Storage:
  state/broker/blueprints/{blueprint_id}.json
"""

import json
import uuid
from typing import Optional, Dict, Any, List
from pathlib import Path
from datetime import datetime, timezone

from brynq.server import BROKER_DIR, _write_json_sync
from brynq.tasks import create_task
from brynq.task_dependencies import DependencyManager

BLUEPRINTS_DIR = BROKER_DIR / "blueprints"
BLUEPRINTS_DIR.mkdir(parents=True, exist_ok=True)

def validate_blueprint(data: dict) -> bool:
    """Validate a blueprint dictionary against the required schema."""
    required_keys = ["name", "version", "agents", "tasks"]
    for req in required_keys:
        if req not in data:
            return False
    if not isinstance(data.get("agents"), list) or not isinstance(data.get("tasks"), list):
        return False
    return True

def publish_blueprint(data: dict) -> dict:
    """Publish a blueprint to the exchange.
    
    Args:
        data: The blueprint definition.
        
    Returns:
        The published blueprint with generated metadata.
        
    Raises:
        ValueError: If the schema is invalid.
    """
    if not validate_blueprint(data):
        raise ValueError("Invalid blueprint schema. Must contain 'name', 'version', 'agents', and 'tasks'.")
    
    blueprint_id = data.get("blueprint_id") or uuid.uuid4().hex[:12]
    data["blueprint_id"] = blueprint_id
    data["created_at"] = data.get("created_at") or datetime.now(timezone.utc).isoformat()
    data["deploy_count"] = data.get("deploy_count", 0)
    
    path = BLUEPRINTS_DIR / f"{blueprint_id}.json"
    _write_json_sync(path, data)
    return data

def get_blueprint(blueprint_id: str) -> Optional[dict]:
    """Retrieve a blueprint by ID."""
    path = BLUEPRINTS_DIR / f"{blueprint_id}.json"
    if not path.exists():
        return None
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return None

def list_blueprints() -> List[dict]:
    """List all available blueprints on the exchange."""
    blueprints = []
    for f in sorted(BLUEPRINTS_DIR.glob("*.json")):
        try:
            data = json.loads(f.read_text("utf-8"))
            blueprints.append(data)
        except (json.JSONDecodeError, OSError):
            continue
    # Sort by popularity (deploy_count)
    blueprints.sort(key=lambda x: x.get("deploy_count", 0), reverse=True)
    return blueprints

def delete_blueprint(blueprint_id: str) -> bool:
    """Delete a blueprint from the exchange. Returns True if deleted."""
    path = BLUEPRINTS_DIR / f"{blueprint_id}.json"
    if path.exists():
        path.unlink()
        return True
    return False

def deploy_blueprint(blueprint_id: str, channel: str = "general") -> dict:
    """Deploy a blueprint to the given channel.
    
    Instantiates tasks based on the blueprint definition, setting up
    the required DAG (Directed Acyclic Graph) of task dependencies.
    
    Args:
        blueprint_id: The ID of the blueprint to deploy.
        channel: The channel where tasks should be posted.
        
    Returns:
        A deployment record containing the IDs of created tasks.
    """
    blueprint = get_blueprint(blueprint_id)
    if not blueprint:
        raise ValueError(f"Blueprint {blueprint_id} not found")
        
    created_tasks = {}
    
    # Track task IDs internally defined in the blueprint (e.g. "task_1", "task_2")
    blueprint_tasks = blueprint.get("tasks", [])
    
    # First pass: create all tasks
    for task_def in blueprint_tasks:
        internal_id = task_def.get("id")
        if not internal_id:
            continue
            
        desc = task_def.get("description", "Blueprint task")
        caps = task_def.get("required_capabilities", [])
        
        # In a real environment we would also use agent definitions to provision containers/instances,
        # but for the orchestrator, posting the specialized tasks is the mechanism.
        full_desc = f"[BLUEPRINT: {blueprint['name']}] {desc}"
        
        title = task_def.get("title", internal_id)
        task_id = create_task(
            channel=channel,
            title=f"[BLUEPRINT] {title}",
            description=full_desc,
            priority=task_def.get("priority", "normal"),
        )
        created_tasks[internal_id] = task_id
        
    # Second pass: set up dependencies
    dep_mgr = DependencyManager()
    for task_def in blueprint_tasks:
        internal_id = task_def.get("id")
        if not internal_id or internal_id not in created_tasks:
            continue
            
        real_task_id = created_tasks[internal_id]
        
        for dep_internal_id in task_def.get("dependencies", []):
            if dep_internal_id in created_tasks:
                real_dep_id = created_tasks[dep_internal_id]
                dep_mgr.add_dependency(real_task_id, real_dep_id)
                
    # Update stats
    blueprint["deploy_count"] = blueprint.get("deploy_count", 0) + 1
    blueprint["updated_at"] = datetime.now(timezone.utc).isoformat()
    _write_json_sync(BLUEPRINTS_DIR / f"{blueprint_id}.json", blueprint)
                
    return {
        "status": "deployed",
        "blueprint_id": blueprint_id,
        "blueprint_name": blueprint.get("name"),
        "tasks_created": created_tasks,
        "channel": channel
    }
