import hashlib
import json
import os
import uuid
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

import requests

from extensions.novyx_ram.sdk import (
    NovyxRAMClient,
    NovyxRAMError,
    AuthenticationError,
)
from sentinel.circuit_breaker import PolicyEvaluator, EvaluationMode
from sentinel.trace_models import SentinelTraceStep, StepType, PolicyStatus
from sentinel.rollback_analyzer import RollbackAnalyzer
from sentinel.surgical_rollback import SurgicalRollback
from security.forensic_rollback import ForensicRollback

from .auth import validate_api_key
from .exceptions import NovyxAuthError, NovyxSecurityError, NovyxStorageError
from .telemetry import TelemetryLogger


class Novyx:
    """
    Unified SDK interface for Novyx Core + RAM + Sentinel.

    Four verbs:
    - remember
    - recall
    - act
    - rollback
    """

    def __init__(
        self,
        api_key: str,
        api_url: str = "https://novyx-ram-api.fly.dev",
        sentinel_url: str = "https://novyx-sentinel.fly.dev",
        session_id: Optional[str] = None,
        strict: bool = False,
    ):
        self.api_key = api_key
        self.api_url = api_url.rstrip("/")
        self.sentinel_url = sentinel_url.rstrip("/")
        self.session_id = session_id or f"session_{uuid.uuid4()}"
        self.strict = strict

        validate_api_key(api_key=self.api_key, api_url=self.api_url)

        try:
            self.ram = NovyxRAMClient(api_key=self.api_key, base_url=self.api_url)
        except AuthenticationError as exc:
            raise NovyxAuthError("Invalid Novyx API key.") from exc
        except NovyxRAMError as exc:
            raise NovyxStorageError(str(exc)) from exc

        self.telemetry = TelemetryLogger(session_id=self.session_id)
        self.policy_evaluator = PolicyEvaluator(mode=EvaluationMode.ENFORCEMENT)

        self._memory_count = 0
        self._action_count = 0

    def _now_iso(self) -> str:
        return datetime.now(timezone.utc).isoformat()

    def _hash_content(self, content: str, tags: List[str]) -> str:
        payload = {
            "content": content,
            "tags": tags,
            "session_id": self.session_id,
        }
        digest = hashlib.sha256(
            json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
        ).hexdigest()
        return f"urn:sha256:{digest}"

    def _forge_debt_warning(self) -> None:
        if not self.strict:
            return

        if self._action_count == 0:
            ratio = f"{self._memory_count}:0"
        else:
            ratio = f"{self._memory_count}:{self._action_count}"

        print(
            "Warning: "
            f"{self._memory_count} memories stored, "
            f"{self._action_count} actions taken. "
            f"Consumption debt ratio: {ratio}"
        )

    def remember(self, content: str, tags: Optional[List[str]] = None) -> Dict[str, Any]:
        tags = tags or []
        content_hash = self._hash_content(content, tags)

        try:
            result = self.ram.store(
                observation=content,
                tags=tags,
                context=f"content_hash={content_hash}",
                agent_id=self.session_id,
            )
        except NovyxRAMError as exc:
            raise NovyxStorageError(str(exc)) from exc

        self._memory_count += 1
        self.telemetry.log_pulse(
            "remember",
            {
                "session_id": self.session_id,
                "memory_id": result.uuid,
                "hash": content_hash,
                "tags": tags,
            },
        )
        self._forge_debt_warning()

        return {
            "id": result.uuid,
            "hash": content_hash,
            "stored_at": self._now_iso(),
        }

    def recall(self, query: str, limit: int = 5) -> List[Dict[str, Any]]:
        try:
            search_result = self.ram.search(query=query, limit=limit)
        except NovyxRAMError as exc:
            raise NovyxStorageError(str(exc)) from exc

        retrieved_at = self._now_iso()
        cleaned = []
        for mem in search_result.memories:
            cleaned.append(
                {
                    "uuid": mem.uuid,
                    "observation": mem.observation,
                    "context": mem.context,
                    "tags": mem.tags,
                    "agent_id": mem.agent_id,
                    "created_at": mem.created_at,
                    "score": mem.score,
                    "query": query,
                    "retrieved_at": retrieved_at,
                    "session_id": self.session_id,
                }
            )

        self.telemetry.log_pulse(
            "recall",
            {
                "session_id": self.session_id,
                "query": query,
                "limit": limit,
                "results": len(cleaned),
            },
        )
        return cleaned

    def act(self, action_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
        self._check_sentinel()
        content = json.dumps({"action": action_name, "params": params}, sort_keys=True)
        integrity_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
        chain_hash = hashlib.sha256(
            (integrity_hash + ("0" * 64)).encode("utf-8")
        ).hexdigest()

        step = SentinelTraceStep(
            trace_id=f"urn:uuid:{self.session_id}",
            step_index=self._action_count,
            step_type=StepType.ACTION,
            content=content,
            policy_status=PolicyStatus.SKIPPED,
            integrity_hash=integrity_hash,
            integrity_chain_hash=chain_hash,
        )

        evaluation = self.policy_evaluator.evaluate_step(step)
        if evaluation.policy_status == PolicyStatus.FAILED:
            violation = evaluation.violations[0]
            raise NovyxSecurityError(
                f"{violation.policy_name}: {violation.reason}"
            )

        self._action_count += 1
        self.telemetry.log_pulse(
            "act",
            {
                "session_id": self.session_id,
                "action": action_name,
                "params": params,
                "policy_status": evaluation.policy_status.value,
                "risk_score": evaluation.risk_score,
            },
        )

        return {
            "action": action_name,
            "status": "approved",
            "policy_status": evaluation.policy_status.value,
            "risk_score": evaluation.risk_score,
        }

    def _check_sentinel(self) -> None:
        try:
            resp = requests.get(f"{self.sentinel_url}/health", timeout=5)
        except requests.RequestException as exc:
            raise NovyxSecurityError("Sentinel is unreachable.") from exc

        if resp.status_code != 200:
            raise NovyxSecurityError(
                f"Sentinel integrity check failed (status {resp.status_code})."
            )

    def rollback(self, target: str) -> Dict[str, Any]:
        memory_dir = os.getenv("NOVYX_MEMORY_DIR", "memory")
        analyzer = RollbackAnalyzer(memory_dir=memory_dir)
        rollback_engine = ForensicRollback(memory_dir=memory_dir)
        surgical = SurgicalRollback(memory_dir=memory_dir, rollback=rollback_engine)

        suggestion = None
        if "T" in target and "Z" in target:
            impact = analyzer.calculate_impact(target)
            result = rollback_engine.execute_rollback(target)
            suggestion = impact.get("suggestion") or "Rollback executed"
            rolled_back = result.get("artifacts_restored", [])
            preserved = result.get("artifacts_quarantined", [])
        else:
            # Treat as artifact_id; rollback to latest green point with dependencies
            points = rollback_engine.list_rollback_points()
            if not points:
                raise NovyxStorageError("No rollback points available.")
            latest = points[-1]
            impact = analyzer.calculate_impact(latest.timestamp)
            suggestion = impact.get("suggestion") or "Surgical rollback executed"
            result = surgical.rollback_artifacts(
                [target],
                latest.timestamp,
                include_dependencies=True,
                dry_run=False,
            ).to_dict()
            rolled_back = result.get("artifacts_rolled_back", [])
            preserved = result.get("artifacts_failed", [])

        self.telemetry.log_pulse(
            "rollback",
            {
                "session_id": self.session_id,
                "target": target,
                "suggestion": suggestion,
            },
        )

        return {
            "rolled_back": rolled_back,
            "preserved": preserved,
            "suggestion": suggestion,
        }
