"""
BizNode Tool Publishing Service
================================
Manages tool manifests, publishing, discovery, and cross-bot tool calling.

Each registered bot/agent can publish tools that other bots can discover and call.
Free tools (cost=0) are called directly; paid tools go through escrow.
"""

import hashlib
import json
import logging
import os
import random
import string
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

_PROJECT_ROOT = str(Path(__file__).resolve().parent.parent)
if _PROJECT_ROOT not in sys.path:
    sys.path.insert(0, _PROJECT_ROOT)

from memory.database import get_connection

log = logging.getLogger(__name__)


def _generate_tool_id() -> str:
    """Generate a unique tool ID like BZTL-A1B2C."""
    chars = string.ascii_uppercase + string.digits
    suffix = "".join(random.choices(chars, k=5))
    return f"BZTL-{suffix}"


class ToolPublishingService:
    """Manages tool publishing, discovery, and calling for the BizNode marketplace."""

    def __init__(self):
        self._db_mod = None

    def _db(self):
        """Lazy-load database module."""
        if self._db_mod is None:
            import importlib.util
            spec = importlib.util.spec_from_file_location(
                "_db", os.path.join(_PROJECT_ROOT, "memory", "database.py")
            )
            mod = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(mod)
            self._db_mod = mod
        return self._db_mod

    # ------------------------------------------------------------------
    # Publishing
    # ------------------------------------------------------------------

    def publish_tools(self, agent_id: str, tools_list: List[Dict]) -> Dict[str, Any]:
        """
        Publish one or more tools for an agent.
        Each tool dict should have: name, description, category, input_schema,
        output_schema (optional), cost_amount (default 0), cost_currency (default DZIT),
        access_level (default public), handler_url (optional).
        """
        db = self._db()
        published = []
        errors = []

        # Resolve node_id
        try:
            from services.mcp_service import get_node_id
            node_id = get_node_id()
        except Exception:
            node_id = "local"

        for tool in tools_list:
            name = tool.get("name", "")
            if not name:
                errors.append({"error": "Tool name required", "tool": tool})
                continue

            tool_id = _generate_tool_id()
            input_schema = tool.get("input_schema", {})
            output_schema = tool.get("output_schema", {})
            if isinstance(input_schema, dict):
                input_schema = json.dumps(input_schema)
            if isinstance(output_schema, dict):
                output_schema = json.dumps(output_schema)

            # Create manifest hash
            manifest = json.dumps({
                "tool_id": tool_id,
                "agent_id": agent_id,
                "name": name,
                "input_schema": input_schema,
                "version": tool.get("version", "1.0.0"),
            }, sort_keys=True)
            manifest_hash = hashlib.sha256(manifest.encode()).hexdigest()

            # Sign manifest
            signature = ""
            try:
                from services.mcp_service import get_or_create_keypair
                priv, _ = get_or_create_keypair()
                sig_bytes = priv.sign(manifest.encode())
                import base64
                signature = base64.urlsafe_b64encode(sig_bytes).rstrip(b"=").decode()
            except Exception as exc:
                log.warning("Could not sign tool manifest: %s", exc)

            try:
                db.create_published_tool(
                    tool_id=tool_id,
                    agent_id=agent_id,
                    node_id=node_id,
                    name=name,
                    description=tool.get("description", ""),
                    category=tool.get("category", "general"),
                    input_schema=input_schema,
                    output_schema=output_schema,
                    cost_amount=float(tool.get("cost_amount", 0)),
                    cost_currency=tool.get("cost_currency", "DZIT"),
                    access_level=tool.get("access_level", "public"),
                    handler_url=tool.get("handler_url", ""),
                    version=tool.get("version", "1.0.0"),
                    manifest_hash=manifest_hash,
                    signature=signature,
                )
                published.append({"tool_id": tool_id, "name": name})
            except Exception as exc:
                errors.append({"error": str(exc), "name": name})

        return {
            "success": len(published) > 0,
            "published": published,
            "errors": errors,
            "total_published": len(published),
        }

    def auto_detect_tools(self) -> List[Dict]:
        """
        Scan the local ToolRegistry for installed tools and return their schemas
        in a format ready for publishing.
        """
        detected = []
        try:
            from automation.registry import get_registry
            registry = get_registry()
            schemas = registry.get_tool_schemas()
            for schema in schemas:
                detected.append({
                    "name": schema.get("name", "unknown"),
                    "description": schema.get("description", ""),
                    "category": schema.get("category", "general"),
                    "input_schema": schema.get("parameters", {}),
                    "cost_amount": 0,
                    "cost_currency": "DZIT",
                    "access_level": "public",
                })
        except Exception as exc:
            log.warning("Auto-detect tools failed: %s", exc)
        return detected

    # ------------------------------------------------------------------
    # Queries
    # ------------------------------------------------------------------

    def get_published_tools(
        self, agent_id: str = None, category: str = None, active_only: bool = True
    ) -> List[Dict]:
        """Query published tools with optional filters."""
        db = self._db()
        if agent_id:
            tools = db.get_published_tools_by_agent(agent_id)
            if active_only:
                tools = [t for t in tools if t.get("is_active")]
            if category:
                tools = [t for t in tools if t.get("category") == category]
            return tools
        return db.list_published_tools(category=category, active_only=active_only)

    def get_tool_manifest(self, tool_id: str) -> Optional[Dict]:
        """Get full tool manifest including parsed schemas."""
        db = self._db()
        tool = db.get_published_tool(tool_id)
        if not tool:
            return None
        # Parse JSON schemas
        for field in ("input_schema", "output_schema"):
            if tool.get(field) and isinstance(tool[field], str):
                try:
                    tool[field] = json.loads(tool[field])
                except (json.JSONDecodeError, TypeError):
                    pass
        return tool

    def search_tools(self, query: str, max_cost: float = None) -> List[Dict]:
        """Search tools by keyword in name/description."""
        db = self._db()
        all_tools = db.list_published_tools(active_only=True)
        q_lower = query.lower()
        results = []
        for t in all_tools:
            name = (t.get("name") or "").lower()
            desc = (t.get("description") or "").lower()
            cat = (t.get("category") or "").lower()
            if q_lower in name or q_lower in desc or q_lower in cat:
                if max_cost is not None and t.get("cost_amount", 0) > max_cost:
                    continue
                results.append(t)
        return results

    # ------------------------------------------------------------------
    # Updates
    # ------------------------------------------------------------------

    def update_tool(self, tool_id: str, **fields) -> Dict[str, Any]:
        """Update tool properties (cost, description, active status, etc.)."""
        allowed = {
            "description", "category", "cost_amount", "cost_currency",
            "access_level", "handler_url", "version", "is_active",
            "input_schema", "output_schema",
        }
        updates = {k: v for k, v in fields.items() if k in allowed}
        if not updates:
            return {"success": False, "error": "No valid fields to update"}

        # Serialize schema fields
        for field in ("input_schema", "output_schema"):
            if field in updates and isinstance(updates[field], dict):
                updates[field] = json.dumps(updates[field])

        db = self._db()
        ok = db.update_published_tool(tool_id, **updates)
        if ok:
            return {"success": True, "tool_id": tool_id}
        return {"success": False, "error": "Tool not found"}

    def deactivate_tool(self, tool_id: str) -> Dict[str, Any]:
        """Unpublish a tool (soft delete)."""
        db = self._db()
        ok = db.update_published_tool(tool_id, is_active=0)
        if ok:
            return {"success": True, "tool_id": tool_id, "status": "deactivated"}
        return {"success": False, "error": "Tool not found"}

    # ------------------------------------------------------------------
    # Cost calculation
    # ------------------------------------------------------------------

    def calculate_tool_cost(self, tool_ids: List[str]) -> Dict[str, Any]:
        """Calculate total cost for calling a chain of tools."""
        db = self._db()
        total_dzit = 0.0
        total_bzeusd = 0.0
        tools_detail = []
        missing = []

        for tid in tool_ids:
            tool = db.get_published_tool(tid)
            if not tool:
                missing.append(tid)
                continue
            cost = tool.get("cost_amount", 0)
            currency = tool.get("cost_currency", "DZIT")
            if currency == "BZeUSD":
                total_bzeusd += cost
            else:
                total_dzit += cost
            tools_detail.append({
                "tool_id": tid,
                "name": tool["name"],
                "cost": cost,
                "currency": currency,
            })

        return {
            "success": True,
            "tools": tools_detail,
            "total_dzit": total_dzit,
            "total_bzeusd": total_bzeusd,
            "missing_tools": missing,
        }

    # ------------------------------------------------------------------
    # Tool calling (proxy)
    # ------------------------------------------------------------------

    def call_tool(
        self, tool_id: str, caller_agent_id: str, params: Dict
    ) -> Dict[str, Any]:
        """
        Call a published tool. For free tools (cost=0), execute directly.
        For paid tools, check balance, deduct, execute, log.
        """
        db = self._db()
        tool = db.get_published_tool(tool_id)
        if not tool:
            return {"success": False, "error": "Tool not found"}
        if not tool.get("is_active"):
            return {"success": False, "error": "Tool is not active"}

        cost = tool.get("cost_amount", 0)
        currency = tool.get("cost_currency", "DZIT")

        # Check and deduct balance for paid tools
        if cost > 0:
            caller = db.get_hosted_agent(caller_agent_id)
            if not caller:
                return {"success": False, "error": "Caller agent not found"}

            balance_field = "dzit_balance" if currency == "DZIT" else "bzeusd_balance"
            if caller.get(balance_field, 0) < cost:
                return {
                    "success": False,
                    "error": f"Insufficient {currency} balance. Need {cost}, have {caller.get(balance_field, 0)}",
                }

            # Deduct from caller
            delta_kwargs = {}
            if currency == "DZIT":
                delta_kwargs["dzit_delta"] = -cost
            else:
                delta_kwargs["bzeusd_delta"] = -cost
            db.adjust_agent_balance(caller_agent_id, **delta_kwargs)

        # Execute the tool
        result = self._execute_tool(tool, params)

        # Credit the tool owner for paid calls
        if cost > 0 and result.get("success"):
            owner_agent_id = tool["agent_id"]
            credit_kwargs = {}
            if currency == "DZIT":
                credit_kwargs["dzit_delta"] = cost
            else:
                credit_kwargs["bzeusd_delta"] = cost
            db.adjust_agent_balance(owner_agent_id, **credit_kwargs)

        # Update call count
        db.increment_tool_call(tool_id, earned=cost if result.get("success") else 0)

        return {
            "success": result.get("success", False),
            "tool_id": tool_id,
            "cost_charged": cost if result.get("success") else 0,
            "currency": currency,
            "result": result.get("data"),
            "error": result.get("error"),
        }

    def _execute_tool(self, tool: Dict, params: Dict) -> Dict:
        """Execute a tool by name using the local ToolRegistry."""
        tool_name = tool.get("name", "")
        try:
            from automation.registry import get_registry
            registry = get_registry()
            if registry.has(tool_name):
                local_tool = registry.get(tool_name)
                result = local_tool.run(**params)
                return {"success": True, "data": result}
            else:
                # Tool not available locally — would need remote call
                handler_url = tool.get("handler_url", "")
                if handler_url:
                    return self._call_remote_tool(handler_url, params)
                return {"success": False, "error": f"Tool '{tool_name}' not available locally or remotely"}
        except Exception as exc:
            log.error("Tool execution failed for %s: %s", tool_name, exc)
            return {"success": False, "error": str(exc)}

    def _call_remote_tool(self, handler_url: str, params: Dict) -> Dict:
        """Call a tool on a remote node via its handler URL."""
        try:
            import requests
            resp = requests.post(
                handler_url,
                json=params,
                timeout=30,
                headers={"X-BizNode-Tool-Call": "true"},
            )
            if resp.ok:
                return {"success": True, "data": resp.json()}
            return {"success": False, "error": f"Remote call failed: {resp.status_code}"}
        except Exception as exc:
            return {"success": False, "error": f"Remote call error: {exc}"}

    # ------------------------------------------------------------------
    # Registry sync
    # ------------------------------------------------------------------

    def sync_to_registry(self, agent_id: str) -> Dict[str, Any]:
        """Push published tools to registry.1bz.biz."""
        db = self._db()
        tools = db.get_published_tools_by_agent(agent_id)
        active_tools = [t for t in tools if t.get("is_active")]

        if not active_tools:
            return {"success": False, "error": "No active tools to sync"}

        # Build manifest
        try:
            from services.mcp_service import get_node_id
            node_id = get_node_id()
        except Exception:
            node_id = "local"

        manifest = {
            "agent_id": agent_id,
            "node_id": node_id,
            "tools": [
                {
                    "tool_id": t["tool_id"],
                    "name": t["name"],
                    "description": t.get("description", ""),
                    "category": t.get("category", "general"),
                    "cost_amount": t.get("cost_amount", 0),
                    "cost_currency": t.get("cost_currency", "DZIT"),
                    "access_level": t.get("access_level", "public"),
                    "version": t.get("version", "1.0.0"),
                }
                for t in active_tools
            ],
            "synced_at": datetime.now(timezone.utc).isoformat(),
        }

        # Push to nodemonitor / registry
        try:
            import requests
            nodemonitor_url = os.getenv("NODEMONITOR_URL", "http://1bz.biz:7333")
            master_key = os.getenv("NODEMONITOR_MASTER_KEY", "")
            resp = requests.post(
                f"{nodemonitor_url}/api/tools/sync",
                json=manifest,
                headers={"X-Master-Key": master_key},
                timeout=15,
            )
            if resp.ok:
                return {"success": True, "synced_tools": len(active_tools)}
            return {"success": False, "error": f"Registry sync failed: {resp.status_code}"}
        except Exception as exc:
            log.warning("Registry sync failed: %s", exc)
            return {"success": False, "error": str(exc)}

    # ------------------------------------------------------------------
    # Identity integration
    # ------------------------------------------------------------------

    def get_tools_for_identity(self, agent_id: str = None) -> List[Dict]:
        """Get tool summary for inclusion in node identity card."""
        db = self._db()
        if agent_id:
            tools = db.get_published_tools_by_agent(agent_id)
        else:
            tools = db.list_published_tools(active_only=True)

        return [
            {
                "tool_id": t["tool_id"],
                "name": t["name"],
                "cost": t.get("cost_amount", 0),
                "currency": t.get("cost_currency", "DZIT"),
            }
            for t in tools
            if t.get("is_active")
        ]
