"""
notilens
========
Unified SDK + CLI for NotiLens push notifications.

Usage (SDK):
    import notilens
    agent = notilens.init(agent="my-agent", token="TOKEN", secret="SECRET")
    agent.task_start(task_id="run-1")
    agent.task_complete("All done", task_id="run-1")

Usage (auto-patch):
    agent = notilens.init(agent="my-agent", token="TOKEN", secret="SECRET", patch=True)
    # Now use OpenAI, Anthropic, LangChain etc. normally — events fire automatically.
"""

from __future__ import annotations

import atexit
import logging
import os
import time
from typing import Optional

from .emitter import NotiLensEmitter
from .loop_detector import LoopDetector
from .state import delete_state, get_state_file, read_state, update_state, write_state

__version__ = "0.2.0"
__all__ = ["init", "NotiLensAgent"]

logger = logging.getLogger("notilens")

# Registry of initialised agents — key: agent name
_agents: dict[str, "NotiLensAgent"] = {}
# The agent that owns auto-patching (only one allowed)
_patch_owner: Optional[str] = None


class NotiLensAgent:
    """
    A configured NotiLens agent. Returned by notilens.init().
    Exposes the full event API and optional auto-patching.
    """

    def __init__(
        self,
        agent: str,
        token: str,
        secret: str,
        *,
        min_level: str = "info",
        loop_threshold: int = 10,
        loop_window: float = 60.0,
        call_timeout: float = 30.0,
        silent: bool = False,
    ):
        self.agent = agent
        self._call_timeout = call_timeout
        self._emitter = NotiLensEmitter(
            token=token,
            secret=secret,
            agent=agent,
            min_level=min_level,
            silent=silent,
        )
        self._loop_detector = LoopDetector(
            threshold=loop_threshold,
            window_seconds=loop_window,
        )

    # ── Generic emit ─────────────────────────────────────────────────────────

    def emit(self, event: str, message: str, level: str = "info", meta: Optional[dict] = None) -> None:
        self._emitter.send(event, message, level=level, meta=meta or {})

    # ── Task lifecycle ────────────────────────────────────────────────────────

    def task_start(self, task_id: Optional[str] = None) -> str:
        task_id = task_id or _auto_task_id()
        sf = get_state_file(self.agent, task_id)
        write_state(sf, {
            "agent": self.agent,
            "task": task_id,
            "start_time": int(time.time() * 1000),
            "retry_count": 0,
        })
        self._emitter.send("task.started", f"{self.agent} | {task_id} started",
                           level="info", meta={"task_id": task_id})
        return task_id

    def task_progress(self, message: str, task_id: str) -> None:
        sf = get_state_file(self.agent, task_id)
        update_state(sf, {"duration_ms": _duration(sf)})
        self._emitter.send("task.progress", message, level="info",
                           meta={"task_id": task_id, **_state_meta(sf)})

    def task_loop(self, message: str, task_id: str) -> None:
        sf = get_state_file(self.agent, task_id)
        state = read_state(sf)
        loop_count = state.get("loop_count", 0) + 1
        update_state(sf, {"duration_ms": _duration(sf), "loop_count": loop_count})
        self._emitter.send("task.loop", message, level="warning",
                           meta={"task_id": task_id, "loop_count": loop_count, **_state_meta(sf)})

    def task_retry(self, task_id: str) -> None:
        sf = get_state_file(self.agent, task_id)
        state = read_state(sf)
        update_state(sf, {"duration_ms": _duration(sf), "retry_count": state.get("retry_count", 0) + 1})
        self._emitter.send("task.retrying", f"{self.agent} | {task_id} retrying", level="warning",
                           meta={"task_id": task_id, **_state_meta(sf)})

    def task_stop(self, task_id: str) -> None:
        sf = get_state_file(self.agent, task_id)
        update_state(sf, {"duration_ms": _duration(sf)})
        self._emitter.send("task.stopped", f"{self.agent} | {task_id} stopped", level="info",
                           meta={"task_id": task_id, **_state_meta(sf)})

    def task_complete(self, message: str, task_id: str) -> None:
        sf = get_state_file(self.agent, task_id)
        update_state(sf, {"duration_ms": _duration(sf)})
        self._emitter.send("task.completed", message, level="info",
                           meta={"task_id": task_id, **_state_meta(sf)})
        delete_state(sf)

    def task_error(self, message: str, task_id: str) -> None:
        sf = get_state_file(self.agent, task_id)
        update_state(sf, {"duration_ms": _duration(sf), "last_error": message})
        self._emitter.send("task.error", message, level="error",
                           meta={"task_id": task_id, **_state_meta(sf)})

    def task_fail(self, message: str, task_id: str) -> None:
        sf = get_state_file(self.agent, task_id)
        update_state(sf, {"duration_ms": _duration(sf)})
        self._emitter.send("task.failed", message, level="error",
                           meta={"task_id": task_id, **_state_meta(sf)})
        delete_state(sf)

    def task_timeout(self, message: str, task_id: str) -> None:
        sf = get_state_file(self.agent, task_id)
        update_state(sf, {"duration_ms": _duration(sf)})
        self._emitter.send("task.timeout", message, level="error",
                           meta={"task_id": task_id, **_state_meta(sf)})
        delete_state(sf)

    def task_cancel(self, message: str, task_id: str) -> None:
        sf = get_state_file(self.agent, task_id)
        update_state(sf, {"duration_ms": _duration(sf)})
        self._emitter.send("task.cancelled", message, level="warning",
                           meta={"task_id": task_id, **_state_meta(sf)})
        delete_state(sf)

    def task_terminate(self, message: str, task_id: str) -> None:
        sf = get_state_file(self.agent, task_id)
        update_state(sf, {"duration_ms": _duration(sf)})
        self._emitter.send("task.terminated", message, level="error",
                           meta={"task_id": task_id, **_state_meta(sf)})
        delete_state(sf)

    # ── Input ─────────────────────────────────────────────────────────────────

    def input_required(self, message: str, task_id: str) -> None:
        self._emitter.send("input.required", message, level="warning",
                           meta={"task_id": task_id})

    def input_approved(self, message: str, task_id: str) -> None:
        self._emitter.send("input.approved", message, level="info",
                           meta={"task_id": task_id})

    def input_rejected(self, message: str, task_id: str) -> None:
        self._emitter.send("input.rejected", message, level="warning",
                           meta={"task_id": task_id})

    # ── AI response ───────────────────────────────────────────────────────────

    def ai_response_generated(self, message: str, task_id: str, meta: Optional[dict] = None) -> None:
        self._emitter.send("ai.response.generated", message, level="info",
                           meta={"task_id": task_id, **(meta or {})})

    def ai_response_failed(self, message: str, task_id: str, meta: Optional[dict] = None) -> None:
        self._emitter.send("ai.response.failed", message, level="error",
                           meta={"task_id": task_id, **(meta or {})})

    # ── Patching ──────────────────────────────────────────────────────────────

    def _apply_patches(self, call_timeout: float) -> list:
        from .patchers import (
            patch_openai, patch_anthropic, patch_langchain,
            patch_crewai, patch_pydantic_ai, patch_http,
        )
        patched = []
        if patch_openai(self._emitter, self._loop_detector, call_timeout):
            patched.append("openai")
        if patch_anthropic(self._emitter, self._loop_detector, call_timeout):
            patched.append("anthropic")
        if patch_langchain(self._emitter, self._loop_detector, call_timeout):
            patched.append("langchain")
        if patch_crewai(self._emitter, self._loop_detector, call_timeout):
            patched.append("crewai")
        if patch_pydantic_ai(self._emitter, self._loop_detector, call_timeout):
            patched.append("pydantic-ai")
        patch_http(self._emitter, self._loop_detector, call_timeout)
        patched.append("http-fallback")
        return patched

    def shutdown(self) -> None:
        self._emitter.shutdown()


# ── Module-level init ─────────────────────────────────────────────────────────

def init(
    agent: str,
    token: Optional[str] = None,
    secret: Optional[str] = None,
    *,
    patch: bool = False,
    min_level: str = "info",
    loop_threshold: int = 10,
    loop_window: float = 60.0,
    call_timeout: float = 30.0,
    silent: bool = False,
    debug: bool = False,
) -> NotiLensAgent:
    """
    Initialise a NotiLens agent.

    Parameters
    ----------
    agent : str
        Agent name. Used to identify the source of events.
    token : str
        NotiLens topic token. Falls back to NOTILENS_TOKEN env var.
    secret : str
        NotiLens topic secret. Falls back to NOTILENS_SECRET env var.
    patch : bool
        Auto-patch AI frameworks (OpenAI, Anthropic, LangChain, etc.). Only one
        agent can own patching — raises if another agent already claimed it.
    """
    global _patch_owner

    if debug:
        logging.basicConfig(level=logging.DEBUG)
        logging.getLogger("notilens").setLevel(logging.DEBUG)

    token = token or os.environ.get("NOTILENS_TOKEN", "")
    secret = secret or os.environ.get("NOTILENS_SECRET", "")

    if not token or not secret:
        raise ValueError(
            "NotiLens token and secret are required. "
            "Pass them to init() or set NOTILENS_TOKEN / NOTILENS_SECRET env vars."
        )

    instance = NotiLensAgent(
        agent=agent,
        token=token,
        secret=secret,
        min_level=min_level,
        loop_threshold=loop_threshold,
        loop_window=loop_window,
        call_timeout=call_timeout,
        silent=silent,
    )

    if patch:
        if _patch_owner is not None and _patch_owner != agent:
            raise RuntimeError(
                f"Patch already claimed by agent '{_patch_owner}'. "
                "Only one agent can own auto-patching."
            )
        _patch_owner = agent
        patched = instance._apply_patches(call_timeout)
        if not silent:
            logger.info("NotiLens: agent '%s' patched %s", agent, ", ".join(patched))

    _agents[agent] = instance
    atexit.register(instance.shutdown)
    return instance


# ── Helpers ───────────────────────────────────────────────────────────────────

def _auto_task_id() -> str:
    return f"task_{int(time.time() * 1000)}"


def _duration(state_file) -> int:
    state = read_state(state_file)
    return int(time.time() * 1000) - state.get("start_time", int(time.time() * 1000))


def _state_meta(state_file) -> dict:
    state = read_state(state_file)
    return {
        "duration_ms": state.get("duration_ms", 0),
        "retry_count": state.get("retry_count", 0),
        "loop_count": state.get("loop_count", 0),
    }
