"""
Unified MCP tools for Data Observability (Phase 3D-4 consolidation).

Consolidates 15 individual observability tools into 4 action-dispatched tools:
- obs_metric(action, ...) -- 4 actions: record, query, stats, list
- obs_alert(action, ...)  -- 5 actions: create_rule, list_rules, list_active, acknowledge, history
- obs_anomaly(action, ...) -- 3 actions: detect, report, configure
- obs_health(action, ...)  -- 3 actions: asset, system, trends

Original tool names are preserved as deprecation wrappers in mcp_tools.py.
"""

import json
import logging
from typing import Any, Dict, Optional

from .types import MetricType, AlertSeverity, AlertStatus
from .metrics_store import MetricsStore
from .alert_manager import AlertManager
from .anomaly_detector import AnomalyDetector
from .health_scorer import HealthScorer
from src.utils.tool_wrapper import safe_tool

logger = logging.getLogger(__name__)

# ============================================================================
# Shared singletons (used by both unified.py and mcp_tools.py wrappers)
# ============================================================================

_metrics_store: Optional[MetricsStore] = None
_alert_manager: Optional[AlertManager] = None
_anomaly_detector: Optional[AnomalyDetector] = None
_health_scorer: Optional[HealthScorer] = None


def _get_data_dir(settings) -> str:
    """Resolve the observability data directory from settings."""
    data_dir = "data/observability"
    if settings and hasattr(settings, "data_dir"):
        data_dir = f"{settings.data_dir}/observability"
    return data_dir


def _ensure_metrics_store(settings) -> MetricsStore:
    """Ensure metrics store singleton is initialized."""
    global _metrics_store
    if _metrics_store is None:
        _metrics_store = MetricsStore(_get_data_dir(settings))
    return _metrics_store


def _ensure_alert_manager(settings) -> AlertManager:
    """Ensure alert manager singleton is initialized."""
    global _alert_manager
    if _alert_manager is None:
        _alert_manager = AlertManager(_get_data_dir(settings))
    return _alert_manager


def _ensure_anomaly_detector(settings) -> AnomalyDetector:
    """Ensure anomaly detector singleton is initialized."""
    global _anomaly_detector
    if _anomaly_detector is None:
        store = _ensure_metrics_store(settings)
        _anomaly_detector = AnomalyDetector(store, _get_data_dir(settings))
    return _anomaly_detector


def _ensure_health_scorer(settings) -> HealthScorer:
    """Ensure health scorer singleton is initialized."""
    global _health_scorer
    if _health_scorer is None:
        store = _ensure_metrics_store(settings)
        alerts = _ensure_alert_manager(settings)
        _health_scorer = HealthScorer(store, alerts, _get_data_dir(settings))
    return _health_scorer


# ============================================================================
# obs_metric action handlers
# ============================================================================

def _metric_record(settings, **kwargs) -> Dict[str, Any]:
    store = _ensure_metrics_store(settings)
    alerts = _ensure_alert_manager(settings)

    name = kwargs.get("name")
    value = kwargs.get("value")
    if not name or value is None:
        return {"error": "name and value are required for 'record' action"}

    metric_type_str = kwargs.get("type", "gauge")
    try:
        metric_type = MetricType(metric_type_str)
    except ValueError:
        return {"error": f"Invalid type. Valid: {[t.value for t in MetricType]}"}

    tags_str = kwargs.get("tags")
    parsed_tags = {}
    if tags_str:
        try:
            parsed_tags = json.loads(tags_str) if isinstance(tags_str, str) else tags_str
        except json.JSONDecodeError:
            return {"error": "Invalid tags JSON"}

    unit = kwargs.get("unit", "")

    metric = store.record_value(name, float(value), metric_type, parsed_tags, unit)

    # Also check for alert triggers
    alert = alerts.evaluate_metric(metric)

    result = metric.model_dump(mode="json")
    if alert:
        result["triggered_alert"] = {
            "id": alert.id,
            "rule_name": alert.rule_name,
            "severity": alert.severity.value,
            "message": alert.message,
        }

    return result


def _metric_query(settings, **kwargs) -> Dict[str, Any]:
    store = _ensure_metrics_store(settings)

    metric_name = kwargs.get("metric_name")
    if not metric_name:
        return {"error": "metric_name is required for 'query' action"}

    hours = kwargs.get("hours", 24)
    limit = min(kwargs.get("limit", 100), 1000)

    tags_str = kwargs.get("tags")
    parsed_tags = None
    if tags_str:
        try:
            parsed_tags = json.loads(tags_str) if isinstance(tags_str, str) else tags_str
        except json.JSONDecodeError:
            return {"error": "Invalid tags JSON"}

    metrics = store.query(metric_name, hours, parsed_tags, limit)

    return {
        "metric_name": metric_name,
        "hours": hours,
        "count": len(metrics),
        "metrics": [m.model_dump(mode="json") for m in metrics[:100]],
    }


def _metric_stats(settings, **kwargs) -> Dict[str, Any]:
    store = _ensure_metrics_store(settings)

    metric_name = kwargs.get("metric_name")
    if not metric_name:
        return {"error": "metric_name is required for 'stats' action"}

    hours = kwargs.get("hours", 24)
    stats = store.aggregate(metric_name, hours)
    return stats.model_dump(mode="json")


def _metric_list(settings, **kwargs) -> Dict[str, Any]:
    store = _ensure_metrics_store(settings)

    names = store.list_metric_names()
    storage_stats = store.get_storage_stats()

    return {
        "metric_names": names,
        "count": len(names),
        "storage": storage_stats,
    }


_METRIC_ACTIONS = {
    "record": _metric_record,
    "query": _metric_query,
    "stats": _metric_stats,
    "list": _metric_list,
}


# ============================================================================
# obs_alert action handlers
# ============================================================================

def _alert_create_rule(settings, **kwargs) -> Dict[str, Any]:
    alerts = _ensure_alert_manager(settings)

    name = kwargs.get("name")
    metric_name = kwargs.get("metric_name")
    threshold = kwargs.get("threshold")
    comparison = kwargs.get("comparison")

    if not name or not metric_name or threshold is None or not comparison:
        return {"error": "name, metric_name, threshold, and comparison are required for 'create_rule' action"}

    if comparison not in [">", "<", ">=", "<=", "==", "!="]:
        return {"error": f"Invalid comparison. Valid: >, <, >=, <=, ==, !="}

    severity_str = kwargs.get("severity", "warning")
    try:
        sev = AlertSeverity(severity_str)
    except ValueError:
        return {"error": f"Invalid severity. Valid: {[s.value for s in AlertSeverity]}"}

    duration_minutes = kwargs.get("duration_minutes", 5)
    description = kwargs.get("description", "")

    rule = alerts.create_rule_from_params(
        name=name,
        metric_name=metric_name,
        threshold=float(threshold),
        comparison=comparison,
        severity=sev,
        duration_minutes=duration_minutes,
        description=description,
    )

    return rule.model_dump(mode="json")


def _alert_list_rules(settings, **kwargs) -> Dict[str, Any]:
    alerts = _ensure_alert_manager(settings)

    enabled_only = kwargs.get("enabled_only", False)
    rules = alerts.list_rules(enabled_only)

    return {
        "count": len(rules),
        "rules": [r.model_dump(mode="json") for r in rules],
    }


def _alert_list_active(settings, **kwargs) -> Dict[str, Any]:
    alerts = _ensure_alert_manager(settings)

    active = alerts.list_active()
    stats = alerts.get_alert_stats()

    return {
        "count": len(active),
        "alerts": [a.model_dump(mode="json") for a in active],
        "stats": stats,
    }


def _alert_acknowledge(settings, **kwargs) -> Dict[str, Any]:
    alerts = _ensure_alert_manager(settings)

    alert_id = kwargs.get("alert_id")
    if not alert_id:
        return {"error": "alert_id is required for 'acknowledge' action"}

    user = kwargs.get("user")
    success = alerts.acknowledge(alert_id, user)

    if not success:
        return {"error": "Alert not found or already resolved"}

    alert = alerts.get_alert(alert_id)
    return {
        "success": True,
        "message": "Alert acknowledged",
        "alert": alert.model_dump(mode="json") if alert else None,
    }


def _alert_history(settings, **kwargs) -> Dict[str, Any]:
    alerts = _ensure_alert_manager(settings)

    hours = kwargs.get("hours", 24)
    include_resolved = kwargs.get("include_resolved", True)

    history = alerts.get_history(hours)

    if not include_resolved:
        history = [a for a in history if a.status != AlertStatus.RESOLVED]

    return {
        "hours": hours,
        "count": len(history),
        "alerts": [a.model_dump(mode="json") for a in history],
    }


_ALERT_ACTIONS = {
    "create_rule": _alert_create_rule,
    "list_rules": _alert_list_rules,
    "list_active": _alert_list_active,
    "acknowledge": _alert_acknowledge,
    "history": _alert_history,
}


# ============================================================================
# obs_anomaly action handlers
# ============================================================================

def _anomaly_detect(settings, **kwargs) -> Dict[str, Any]:
    detector = _ensure_anomaly_detector(settings)

    metric_name = kwargs.get("metric_name")
    value = kwargs.get("value")
    if not metric_name or value is None:
        return {"error": "metric_name and value are required for 'detect' action"}

    tags_str = kwargs.get("tags")
    parsed_tags = {}
    if tags_str:
        try:
            parsed_tags = json.loads(tags_str) if isinstance(tags_str, str) else tags_str
        except json.JSONDecodeError:
            return {"error": "Invalid tags JSON"}

    anomaly = detector.detect(metric_name, float(value), parsed_tags)
    baseline = detector.get_baseline(metric_name)

    if anomaly:
        return {
            "is_anomaly": True,
            "anomaly": anomaly.model_dump(mode="json"),
            "baseline": baseline,
        }
    else:
        return {
            "is_anomaly": False,
            "value": float(value),
            "baseline": baseline,
            "message": "Value is within normal range",
        }


def _anomaly_report(settings, **kwargs) -> Dict[str, Any]:
    detector = _ensure_anomaly_detector(settings)

    metric_name = kwargs.get("metric_name")
    if not metric_name:
        return {"error": "metric_name is required for 'report' action"}

    hours = kwargs.get("hours", 24)
    return detector.get_anomaly_report(metric_name, hours)


def _anomaly_configure(settings, **kwargs) -> Dict[str, Any]:
    detector = _ensure_anomaly_detector(settings)

    config = detector.configure(
        zscore_threshold=kwargs.get("zscore_threshold"),
        min_data_points=kwargs.get("min_data_points"),
        baseline_hours=kwargs.get("baseline_hours"),
        sensitivity=kwargs.get("sensitivity"),
    )

    return {
        "message": "Anomaly detection configured",
        "config": config.model_dump(),
    }


_ANOMALY_ACTIONS = {
    "detect": _anomaly_detect,
    "report": _anomaly_report,
    "configure": _anomaly_configure,
}


# ============================================================================
# obs_health action handlers
# ============================================================================

def _health_asset(settings, **kwargs) -> Dict[str, Any]:
    scorer = _ensure_health_scorer(settings)

    asset_id = kwargs.get("asset_id")
    asset_type = kwargs.get("asset_type")
    if not asset_id or not asset_type:
        return {"error": "asset_id and asset_type are required for 'asset' action"}

    health = scorer.calculate_asset_health(asset_id, asset_type)
    return health.model_dump(mode="json")


def _health_system(settings, **kwargs) -> Dict[str, Any]:
    scorer = _ensure_health_scorer(settings)
    return scorer.get_system_health()


def _health_trends(settings, **kwargs) -> Dict[str, Any]:
    scorer = _ensure_health_scorer(settings)

    asset_id = kwargs.get("asset_id")
    asset_type = kwargs.get("asset_type")
    if not asset_id or not asset_type:
        return {"error": "asset_id and asset_type are required for 'trends' action"}

    hours = kwargs.get("hours", 168)
    trend = scorer.get_health_trend(asset_id, asset_type, hours)
    return trend.model_dump(mode="json")


_HEALTH_ACTIONS = {
    "asset": _health_asset,
    "system": _health_system,
    "trends": _health_trends,
}


# ============================================================================
# Unified tool dispatch functions (called by deprecation wrappers too)
# ============================================================================

def dispatch_obs_metric(settings, action: str, **kwargs) -> Dict[str, Any]:
    """Dispatch an obs_metric action."""
    handler = _METRIC_ACTIONS.get(action)
    if not handler:
        return {
            "error": f"Unknown action: '{action}'",
            "valid_actions": sorted(_METRIC_ACTIONS.keys()),
        }
    try:
        return handler(settings, **kwargs)
    except ValueError as e:
        return {"error": str(e)}
    except Exception as e:
        logger.error(f"obs_metric({action}) failed: {e}")
        return {"error": f"obs_metric({action}) failed: {e}"}


def dispatch_obs_alert(settings, action: str, **kwargs) -> Dict[str, Any]:
    """Dispatch an obs_alert action."""
    handler = _ALERT_ACTIONS.get(action)
    if not handler:
        return {
            "error": f"Unknown action: '{action}'",
            "valid_actions": sorted(_ALERT_ACTIONS.keys()),
        }
    try:
        return handler(settings, **kwargs)
    except ValueError as e:
        return {"error": str(e)}
    except Exception as e:
        logger.error(f"obs_alert({action}) failed: {e}")
        return {"error": f"obs_alert({action}) failed: {e}"}


def dispatch_obs_anomaly(settings, action: str, **kwargs) -> Dict[str, Any]:
    """Dispatch an obs_anomaly action."""
    handler = _ANOMALY_ACTIONS.get(action)
    if not handler:
        return {
            "error": f"Unknown action: '{action}'",
            "valid_actions": sorted(_ANOMALY_ACTIONS.keys()),
        }
    try:
        return handler(settings, **kwargs)
    except ValueError as e:
        return {"error": str(e)}
    except Exception as e:
        logger.error(f"obs_anomaly({action}) failed: {e}")
        return {"error": f"obs_anomaly({action}) failed: {e}"}


def dispatch_obs_health(settings, action: str, **kwargs) -> Dict[str, Any]:
    """Dispatch an obs_health action."""
    handler = _HEALTH_ACTIONS.get(action)
    if not handler:
        return {
            "error": f"Unknown action: '{action}'",
            "valid_actions": sorted(_HEALTH_ACTIONS.keys()),
        }
    try:
        return handler(settings, **kwargs)
    except ValueError as e:
        return {"error": str(e)}
    except Exception as e:
        logger.error(f"obs_health({action}) failed: {e}")
        return {"error": f"obs_health({action}) failed: {e}"}


# ============================================================================
# Registration
# ============================================================================

def register_unified_observability_tools(mcp, settings):
    """Register the 4 unified observability MCP tools."""

    @mcp.tool()
    @safe_tool("obs_metric")
    def obs_metric(
        action: str,
        name: Optional[str] = None,
        value: Optional[float] = None,
        type: str = "gauge",
        tags: Optional[str] = None,
        unit: str = "",
        metric_name: Optional[str] = None,
        hours: int = 24,
        limit: int = 100,
    ) -> Dict[str, Any]:
        """
        Unified metrics management tool. Replaces 4 individual tools.

        Actions:
        - record: Record a metric data point (requires name, value)
        - query: Query metrics by name and time range (requires metric_name)
        - stats: Get aggregated statistics for a metric (requires metric_name)
        - list: List all available metric names

        Args:
            action: The action to perform (see list above)
            name: Metric name for recording (e.g., "hierarchy.validation.success_rate")
            value: Numeric value to record
            type: Metric type - "gauge", "counter", "histogram" (for record)
            tags: JSON object of tags for filtering (for record/query)
            unit: Optional unit e.g. "percent", "ms" (for record)
            metric_name: Metric name for querying (for query/stats)
            hours: Hours to look back (for query/stats, default 24)
            limit: Maximum results (for query, default 100, max 1000)

        Returns:
            Action-specific result dict
        """
        return dispatch_obs_metric(settings, action, **{
            k: v for k, v in {
                "name": name, "value": value, "type": type, "tags": tags,
                "unit": unit, "metric_name": metric_name, "hours": hours,
                "limit": limit,
            }.items() if v is not None or k in ("value",)
        })

    @mcp.tool()
    @safe_tool("obs_alert")
    def obs_alert(
        action: str,
        name: Optional[str] = None,
        metric_name: Optional[str] = None,
        threshold: Optional[float] = None,
        comparison: Optional[str] = None,
        severity: str = "warning",
        duration_minutes: int = 5,
        description: str = "",
        enabled_only: bool = False,
        alert_id: Optional[str] = None,
        user: Optional[str] = None,
        hours: int = 24,
        include_resolved: bool = True,
    ) -> Dict[str, Any]:
        """
        Unified alerting management tool. Replaces 5 individual tools.

        Actions:
        - create_rule: Create a threshold-based alert rule (requires name, metric_name, threshold, comparison)
        - list_rules: List all alert rules
        - list_active: List active (unresolved) alerts
        - acknowledge: Acknowledge an alert (requires alert_id)
        - history: Get historical alerts

        Args:
            action: The action to perform (see list above)
            name: Rule name (for create_rule)
            metric_name: Metric to monitor (for create_rule)
            threshold: Threshold value (for create_rule)
            comparison: Operator >, <, >=, <=, ==, != (for create_rule)
            severity: Alert severity - info, warning, critical (for create_rule)
            duration_minutes: How long condition must persist (for create_rule, default 5)
            description: Rule description (for create_rule)
            enabled_only: Only show enabled rules (for list_rules)
            alert_id: Alert ID to acknowledge (for acknowledge)
            user: User acknowledging (for acknowledge)
            hours: Time window in hours (for history, default 24)
            include_resolved: Include resolved alerts (for history, default true)

        Returns:
            Action-specific result dict
        """
        return dispatch_obs_alert(settings, action, **{
            k: v for k, v in {
                "name": name, "metric_name": metric_name, "threshold": threshold,
                "comparison": comparison, "severity": severity,
                "duration_minutes": duration_minutes, "description": description,
                "enabled_only": enabled_only, "alert_id": alert_id, "user": user,
                "hours": hours, "include_resolved": include_resolved,
            }.items() if v is not None or k in ("threshold",)
        })

    @mcp.tool()
    @safe_tool("obs_anomaly")
    def obs_anomaly(
        action: str,
        metric_name: Optional[str] = None,
        value: Optional[float] = None,
        tags: Optional[str] = None,
        hours: int = 24,
        zscore_threshold: Optional[float] = None,
        min_data_points: Optional[int] = None,
        baseline_hours: Optional[int] = None,
        sensitivity: Optional[float] = None,
    ) -> Dict[str, Any]:
        """
        Unified anomaly detection tool. Replaces 3 individual tools.

        Actions:
        - detect: Check if a value is anomalous (requires metric_name, value)
        - report: Get recent anomalies for a metric (requires metric_name)
        - configure: Set anomaly detection thresholds

        Args:
            action: The action to perform (see list above)
            metric_name: Metric name (for detect/report)
            value: Value to check (for detect)
            tags: JSON tags for context (for detect)
            hours: Time window in hours (for report, default 24)
            zscore_threshold: Z-score threshold (for configure, default 3.0)
            min_data_points: Minimum data points for baseline (for configure, default 10)
            baseline_hours: Hours of history for baseline (for configure, default 168)
            sensitivity: Threshold multiplier (for configure, default 1.0)

        Returns:
            Action-specific result dict
        """
        return dispatch_obs_anomaly(settings, action, **{
            k: v for k, v in {
                "metric_name": metric_name, "value": value, "tags": tags,
                "hours": hours, "zscore_threshold": zscore_threshold,
                "min_data_points": min_data_points, "baseline_hours": baseline_hours,
                "sensitivity": sensitivity,
            }.items() if v is not None or k in ("value",)
        })

    @mcp.tool()
    @safe_tool("obs_health")
    def obs_health(
        action: str,
        asset_id: Optional[str] = None,
        asset_type: Optional[str] = None,
        hours: int = 168,
    ) -> Dict[str, Any]:
        """
        Unified health scoring tool. Replaces 3 individual tools.

        Actions:
        - asset: Get health score for an asset (requires asset_id, asset_type)
        - system: Get overall system health dashboard
        - trends: Get health score trends over time (requires asset_id, asset_type)

        Args:
            action: The action to perform (see list above)
            asset_id: Asset identifier (for asset/trends)
            asset_type: Asset type e.g. "hierarchy_project", "catalog_asset" (for asset/trends)
            hours: Time window in hours (for trends, default 168 = 1 week)

        Returns:
            Action-specific result dict
        """
        return dispatch_obs_health(settings, action, **{
            k: v for k, v in {
                "asset_id": asset_id, "asset_type": asset_type, "hours": hours,
            }.items() if v is not None
        })

    logger.info("Registered 4 unified observability tools: obs_metric, obs_alert, obs_anomaly, obs_health")
    return ["obs_metric", "obs_alert", "obs_anomaly", "obs_health"]
