from __future__ import annotations

import asyncio
import logging
import queue
import threading
import time
from dataclasses import asdict
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

from . import _transport as _t
from .exceptions import (
    LogguardAuthError,
    LogguardError,
    LogguardNotInitializedError,
    LogguardValidationError,
)
from .models import (
    AlertCondition,
    AlertRule,
    BlacklistEntry,
    Event,
    IngestResult,
)

log = logging.getLogger("loguard")

_DEFAULT_TIMEOUT = 10.0
_DEFAULT_RETRIES = 3
_DEFAULT_BASE_URL = "https://api.logguard.io"

_FAF_FLUSH_INTERVAL = 0.25
_FAF_MAX_BATCH = 50
_FAF_QUEUE_MAX = 2000


class _Monitor:
    """
    Global Logguard monitor singleton.

    Usage:
        from logguard import monitor

        monitor.init(api_key="lg_live_xxx", env="production")

        # Track an event
        monitor.event(type="login_failed", ip="1.2.3.4", path="/login", status_code=401)

        # Create a custom alert rule
        from logguard.models import AlertRule, AlertCondition
        rule = monitor.alerts.create(AlertRule(
            name="Brute force",
            conditions=[
                AlertCondition(field="type", op="eq", value="login_failed"),
                AlertCondition(field="rate_per_minute", op="gt", value=10),
            ],
            severity="high",
            actions=["notify", "block"],
        ))

        # Blacklist an IP
        from logguard.models import BlacklistEntry
        monitor.blacklist.add(BlacklistEntry(type="ip", value="1.2.3.4", reason="Scan"))
    """

    def __init__(self):
        self._api_key: Optional[str] = None
        self._base_url: str = _DEFAULT_BASE_URL
        self._env: str = "production"
        self._timeout: float = _DEFAULT_TIMEOUT
        self._retries: int = _DEFAULT_RETRIES
        self._initialized: bool = False

        self._faf_q: "queue.Queue[Event]" = queue.Queue(maxsize=_FAF_QUEUE_MAX)
        self._faf_thread: Optional[threading.Thread] = None
        self._faf_stop = threading.Event()

        # Sub-clients — available after init()
        self.alerts: _AlertsClient = _AlertsClient(self)
        self.blacklist: _BlacklistClient = _BlacklistClient(self)

    # ── Init ──────────────────────────────────────────────────────────────────

    def init(
        self,
        *,
        api_key: str,
        base_url: str = _DEFAULT_BASE_URL,
        env: str = "production",
        timeout: float = _DEFAULT_TIMEOUT,
        retries: int = _DEFAULT_RETRIES,
        debug: bool = False,
    ) -> None:
        """
        Initialize the Logguard monitor. Call once at application startup.

        Args:
            api_key:   Your Logguard API key (from the dashboard)
            base_url:  Logguard API base URL (default: https://api.logguard.io)
            env:       Environment tag — "production", "staging", "development"
            timeout:   HTTP request timeout in seconds (default: 10)
            retries:   Retry attempts on server errors (default: 3)
            debug:     Enable verbose debug logging
        """
        if not api_key or not api_key.strip():
            raise LogguardAuthError("api_key is required")

        self._api_key = api_key.strip()
        self._base_url = base_url.rstrip("/")
        self._env = env or "production"
        self._timeout = float(timeout)
        self._retries = int(retries)
        self._initialized = True

        if debug:
            logging.basicConfig(level=logging.DEBUG)
            log.setLevel(logging.DEBUG)

        if not self._faf_thread or not self._faf_thread.is_alive():
            self._faf_stop.clear()
            self._faf_thread = threading.Thread(target=self._faf_worker, daemon=True)
            self._faf_thread.start()

        log.debug("Logguard initialized env=%s base_url=%s", self._env, self._base_url)

    # ── Ingest: sync ──────────────────────────────────────────────────────────

    def event(
        self,
        *,
        type: str,
        ip: str,
        path: str,
        status_code: int,
        user_id: Optional[str] = None,
        meta: Optional[Dict[str, Any]] = None,
        ts: Optional[datetime] = None,
    ) -> IngestResult:
        """Track a single security event (synchronous, blocking)."""
        self._assert_initialized()
        ev = self._build_event(
            type=type, ip=ip, path=path, status_code=status_code,
            user_id=user_id, meta=meta, ts=ts,
        )
        return self._send_events_sync([ev])

    def event_batch(self, events: List[Dict[str, Any]]) -> IngestResult:
        """Track multiple events in one request (synchronous)."""
        self._assert_initialized()
        built = [self._build_event(**e) for e in events]
        return self._send_events_sync(built)

    def event_fire_and_forget(
        self,
        *,
        type: str,
        ip: str,
        path: str,
        status_code: int,
        user_id: Optional[str] = None,
        meta: Optional[Dict[str, Any]] = None,
        ts: Optional[datetime] = None,
    ) -> None:
        """
        Track event via background queue — never blocks, never raises.
        Ideal for production middleware (high-traffic apps).
        """
        self._assert_initialized()
        try:
            ev = self._build_event(
                type=type, ip=ip, path=path, status_code=status_code,
                user_id=user_id, meta=meta, ts=ts,
            )
            self._faf_q.put_nowait(ev)
        except Exception:
            return

    # ── Ingest: async ─────────────────────────────────────────────────────────

    async def aevent(
        self,
        *,
        type: str,
        ip: str,
        path: str,
        status_code: int,
        user_id: Optional[str] = None,
        meta: Optional[Dict[str, Any]] = None,
        ts: Optional[datetime] = None,
    ) -> IngestResult:
        """Async version of event()."""
        self._assert_initialized()
        ev = self._build_event(
            type=type, ip=ip, path=path, status_code=status_code,
            user_id=user_id, meta=meta, ts=ts,
        )
        return await self._send_events_async([ev])

    async def aevent_batch(self, events: List[Dict[str, Any]]) -> IngestResult:
        """Async version of event_batch()."""
        self._assert_initialized()
        built = [self._build_event(**e) for e in events]
        return await self._send_events_async(built)

    # ── Properties ────────────────────────────────────────────────────────────

    @property
    def is_initialized(self) -> bool:
        return self._initialized

    # ── Internal ──────────────────────────────────────────────────────────────

    def _assert_initialized(self):
        if not self._initialized:
            raise LogguardNotInitializedError(
                "Logguard is not initialized. Call monitor.init(api_key=...) first."
            )

    @property
    def _headers(self) -> dict:
        return {
            "X-Api-Key": self._api_key,
            "Content-Type": "application/json",
            "User-Agent": "logguard-python-sdk/2.0.0",
        }

    @property
    def _ingest_url(self) -> str:
        return f"{self._base_url}/v1/ingest"

    def _build_event(
        self,
        *,
        type: str,
        ip: str,
        path: str,
        status_code: int,
        user_id: Optional[str] = None,
        meta: Optional[Dict[str, Any]] = None,
        ts: Optional[datetime] = None,
        **_,
    ) -> Event:
        if not type:
            raise LogguardValidationError("event type is required")
        if not ip:
            raise LogguardValidationError("event ip is required")
        if not path:
            raise LogguardValidationError("event path is required")

        sc = int(status_code)
        if sc < 100 or sc > 599:
            raise LogguardValidationError("status_code must be in range 100..599")

        p = str(path).strip()
        if not p.startswith("/"):
            p = "/" + p
        if len(p) > 2048:
            p = p[:2048]

        m = dict(meta) if meta else {}
        m["env"] = self._env

        return Event(
            type=str(type).strip().lower()[:64],
            ip=str(ip).strip()[:64],
            path=p,
            status_code=sc,
            user_id=str(user_id)[:128] if user_id else None,
            meta=m,
            ts=(ts or datetime.now(timezone.utc)).isoformat(),
        )

    def _send_events_sync(self, events: List[Event]) -> IngestResult:
        payload = {"events": [asdict(e) for e in events]}
        data = _t.send_sync(
            self._ingest_url, self._headers, payload,
            self._timeout, self._retries,
        )
        return _t.parse_ingest_response(data)

    async def _send_events_async(self, events: List[Event]) -> IngestResult:
        payload = {"events": [asdict(e) for e in events]}
        data = await _t.send_async(
            self._ingest_url, self._headers, payload,
            self._timeout, self._retries,
        )
        return _t.parse_ingest_response(data)

    def _faf_worker(self) -> None:
        pending: List[Event] = []
        last_flush = time.time()

        while not self._faf_stop.is_set():
            try:
                ev = self._faf_q.get(timeout=0.1)
                pending.append(ev)
            except queue.Empty:
                pass

            now = time.time()
            should_flush = (
                pending
                and (len(pending) >= _FAF_MAX_BATCH or (now - last_flush) >= _FAF_FLUSH_INTERVAL)
            )

            if should_flush:
                batch = pending[:_FAF_MAX_BATCH]
                pending = pending[_FAF_MAX_BATCH:]
                last_flush = now
                try:
                    self._send_events_sync(batch)
                except LogguardError:
                    continue


# ── Alerts sub-client ─────────────────────────────────────────────────────────

class _AlertsClient:
    """
    Manage user-defined alert rules.

    Access via:  monitor.alerts

    Example:
        from logguard.models import AlertRule, AlertCondition

        # Create a rule that fires when an IP makes >10 failed logins/min
        rule = monitor.alerts.create(AlertRule(
            name="Brute force",
            conditions=[
                AlertCondition(field="type",           op="eq",  value="login_failed"),
                AlertCondition(field="rate_per_minute", op="gt", value=10),
            ],
            severity="high",
            actions=["notify", "block"],
        ))
        print(rule.id)  # assigned by server

        # List all rules
        rules = monitor.alerts.list()

        # Update
        rule.enabled = False
        monitor.alerts.update(rule)

        # Delete
        monitor.alerts.delete(rule.id)
    """

    def __init__(self, mon: "_Monitor"):
        self._mon = mon

    def _url(self, rule_id: Optional[int] = None) -> str:
        base = f"{self._mon._base_url}/v1/alert-rules"
        return f"{base}/{rule_id}" if rule_id is not None else base

    # ── Sync ──────────────────────────────────────────────────────────────────

    def create(self, rule: AlertRule) -> AlertRule:
        """Create a new alert rule. Returns the rule with server-assigned id."""
        self._mon._assert_initialized()
        data = _t.send_sync(
            self._url(), self._mon._headers,
            rule.to_dict(), self._mon._timeout, self._mon._retries,
        )
        return AlertRule.from_dict(data if isinstance(data, dict) else {})

    def list(self) -> List[AlertRule]:
        """List all alert rules for this project."""
        self._mon._assert_initialized()
        data = _t.send_sync_no_body(
            self._url(), self._mon._headers,
            self._mon._timeout, self._mon._retries, method="GET",
        )
        items = data if isinstance(data, list) else (data or {}).get("rules", [])
        return [AlertRule.from_dict(d) for d in (items or [])]

    def get(self, rule_id: int) -> AlertRule:
        """Fetch a single alert rule by ID."""
        self._mon._assert_initialized()
        data = _t.send_sync_no_body(
            self._url(rule_id), self._mon._headers,
            self._mon._timeout, self._mon._retries, method="GET",
        )
        return AlertRule.from_dict(data if isinstance(data, dict) else {})

    def update(self, rule: AlertRule) -> AlertRule:
        """Update an existing alert rule (rule.id must be set)."""
        self._mon._assert_initialized()
        if rule.id is None:
            raise LogguardValidationError("rule.id is required to update")
        data = _t.send_sync(
            self._url(rule.id), self._mon._headers,
            rule.to_dict(), self._mon._timeout, self._mon._retries,
            method="PUT",
        )
        return AlertRule.from_dict(data if isinstance(data, dict) else {})

    def delete(self, rule_id: int) -> None:
        """Delete an alert rule by ID."""
        self._mon._assert_initialized()
        _t.send_sync_no_body(
            self._url(rule_id), self._mon._headers,
            self._mon._timeout, self._mon._retries, method="DELETE",
        )

    def enable(self, rule_id: int) -> AlertRule:
        """Enable an alert rule."""
        rule = self.get(rule_id)
        rule.enabled = True
        return self.update(rule)

    def disable(self, rule_id: int) -> AlertRule:
        """Disable an alert rule without deleting it."""
        rule = self.get(rule_id)
        rule.enabled = False
        return self.update(rule)

    # ── Async ─────────────────────────────────────────────────────────────────

    async def acreate(self, rule: AlertRule) -> AlertRule:
        """Async version of create()."""
        self._mon._assert_initialized()
        data = await _t.send_async(
            self._url(), self._mon._headers,
            rule.to_dict(), self._mon._timeout, self._mon._retries,
        )
        return AlertRule.from_dict(data if isinstance(data, dict) else {})

    async def alist(self) -> List[AlertRule]:
        """Async version of list()."""
        self._mon._assert_initialized()
        data = await _t.send_async_no_body(
            self._url(), self._mon._headers,
            self._mon._timeout, self._mon._retries, method="GET",
        )
        items = data if isinstance(data, list) else (data or {}).get("rules", [])
        return [AlertRule.from_dict(d) for d in (items or [])]

    async def aupdate(self, rule: AlertRule) -> AlertRule:
        """Async version of update()."""
        self._mon._assert_initialized()
        if rule.id is None:
            raise LogguardValidationError("rule.id is required to update")
        data = await _t.send_async(
            self._url(rule.id), self._mon._headers,
            rule.to_dict(), self._mon._timeout, self._mon._retries,
            method="PUT",
        )
        return AlertRule.from_dict(data if isinstance(data, dict) else {})

    async def adelete(self, rule_id: int) -> None:
        """Async version of delete()."""
        self._mon._assert_initialized()
        await _t.send_async_no_body(
            self._url(rule_id), self._mon._headers,
            self._mon._timeout, self._mon._retries, method="DELETE",
        )


# ── Blacklist sub-client ──────────────────────────────────────────────────────

class _BlacklistClient:
    """
    Manage the blacklist — block or flag specific IPs, users, CIDRs, paths.

    Access via:  monitor.blacklist

    Example:
        from logguard.models import BlacklistEntry

        # Block an IP permanently
        entry = monitor.blacklist.add(BlacklistEntry(
            type="ip",
            value="185.220.101.1",
            reason="Known Tor exit node",
            action="block",
        ))

        # Block a CIDR range
        monitor.blacklist.add(BlacklistEntry(
            type="cidr",
            value="10.0.0.0/8",
            reason="Internal range misuse",
            action="flag",
        ))

        # Block a user temporarily (expires in 1 hour)
        from datetime import datetime, timedelta, timezone
        monitor.blacklist.add(BlacklistEntry(
            type="user_id",
            value="user_abc123",
            reason="Suspected fraud",
            action="block",
            expires_at=(datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),
        ))

        # List all entries
        entries = monitor.blacklist.list()

        # Remove
        monitor.blacklist.remove(entry.id)

        # Check if an IP/user is blacklisted
        result = monitor.blacklist.check(type="ip", value="185.220.101.1")
        if result["blacklisted"]:
            print("Blocked:", result["reason"])
    """

    def __init__(self, mon: "_Monitor"):
        self._mon = mon

    def _url(self, entry_id: Optional[int] = None) -> str:
        base = f"{self._mon._base_url}/v1/blacklist"
        return f"{base}/{entry_id}" if entry_id is not None else base

    def _check_url(self) -> str:
        return f"{self._mon._base_url}/v1/blacklist/check"

    # ── Sync ──────────────────────────────────────────────────────────────────

    def add(self, entry: BlacklistEntry) -> BlacklistEntry:
        """Add an entry to the blacklist. Returns the entry with server-assigned id."""
        self._mon._assert_initialized()
        data = _t.send_sync(
            self._url(), self._mon._headers,
            entry.to_dict(), self._mon._timeout, self._mon._retries,
        )
        return BlacklistEntry.from_dict(data if isinstance(data, dict) else {})

    def list(
        self,
        type: Optional[str] = None,
        enabled_only: bool = False,
    ) -> List[BlacklistEntry]:
        """
        List blacklist entries.

        Args:
            type:         Filter by type ("ip", "user_id", "cidr", "path", "user_agent")
            enabled_only: Only return active entries
        """
        self._mon._assert_initialized()
        params = []
        if type:
            params.append(f"type={type}")
        if enabled_only:
            params.append("enabled=true")
        url = self._url()
        if params:
            url += "?" + "&".join(params)
        data = _t.send_sync_no_body(
            url, self._mon._headers,
            self._mon._timeout, self._mon._retries, method="GET",
        )
        items = data if isinstance(data, list) else (data or {}).get("entries", [])
        return [BlacklistEntry.from_dict(d) for d in (items or [])]

    def get(self, entry_id: int) -> BlacklistEntry:
        """Fetch a single blacklist entry by ID."""
        self._mon._assert_initialized()
        data = _t.send_sync_no_body(
            self._url(entry_id), self._mon._headers,
            self._mon._timeout, self._mon._retries, method="GET",
        )
        return BlacklistEntry.from_dict(data if isinstance(data, dict) else {})

    def update(self, entry: BlacklistEntry) -> BlacklistEntry:
        """Update a blacklist entry (entry.id must be set)."""
        self._mon._assert_initialized()
        if entry.id is None:
            raise LogguardValidationError("entry.id is required to update")
        data = _t.send_sync(
            self._url(entry.id), self._mon._headers,
            entry.to_dict(), self._mon._timeout, self._mon._retries,
            method="PUT",
        )
        return BlacklistEntry.from_dict(data if isinstance(data, dict) else {})

    def remove(self, entry_id: int) -> None:
        """Remove a blacklist entry by ID."""
        self._mon._assert_initialized()
        _t.send_sync_no_body(
            self._url(entry_id), self._mon._headers,
            self._mon._timeout, self._mon._retries, method="DELETE",
        )

    def check(self, *, type: str, value: str) -> Dict[str, Any]:
        """
        Check if a value is blacklisted.

        Returns a dict:
            {
                "blacklisted": bool,
                "action": "block" | "flag" | "alert" | None,
                "reason": str | None,
                "entry_id": int | None,
            }
        """
        self._mon._assert_initialized()
        data = _t.send_sync(
            self._check_url(), self._mon._headers,
            {"type": type, "value": value},
            self._mon._timeout, self._mon._retries,
        )
        return data if isinstance(data, dict) else {"blacklisted": False}

    def block_ip(self, ip: str, reason: str = "", expires_at: Optional[str] = None) -> BlacklistEntry:
        """Convenience: block an IP address."""
        return self.add(BlacklistEntry(
            type="ip", value=ip, reason=reason, action="block", expires_at=expires_at,
        ))

    def flag_ip(self, ip: str, reason: str = "") -> BlacklistEntry:
        """Convenience: flag (but not block) an IP address."""
        return self.add(BlacklistEntry(
            type="ip", value=ip, reason=reason, action="flag",
        ))

    def block_user(self, user_id: str, reason: str = "", expires_at: Optional[str] = None) -> BlacklistEntry:
        """Convenience: block a user ID."""
        return self.add(BlacklistEntry(
            type="user_id", value=user_id, reason=reason, action="block", expires_at=expires_at,
        ))

    def block_cidr(self, cidr: str, reason: str = "") -> BlacklistEntry:
        """Convenience: block an entire CIDR range."""
        return self.add(BlacklistEntry(
            type="cidr", value=cidr, reason=reason, action="block",
        ))

    # ── Async ─────────────────────────────────────────────────────────────────

    async def aadd(self, entry: BlacklistEntry) -> BlacklistEntry:
        """Async version of add()."""
        self._mon._assert_initialized()
        data = await _t.send_async(
            self._url(), self._mon._headers,
            entry.to_dict(), self._mon._timeout, self._mon._retries,
        )
        return BlacklistEntry.from_dict(data if isinstance(data, dict) else {})

    async def alist(self, type: Optional[str] = None, enabled_only: bool = False) -> List[BlacklistEntry]:
        """Async version of list()."""
        self._mon._assert_initialized()
        params = []
        if type:
            params.append(f"type={type}")
        if enabled_only:
            params.append("enabled=true")
        url = self._url()
        if params:
            url += "?" + "&".join(params)
        data = await _t.send_async_no_body(
            url, self._mon._headers,
            self._mon._timeout, self._mon._retries, method="GET",
        )
        items = data if isinstance(data, list) else (data or {}).get("entries", [])
        return [BlacklistEntry.from_dict(d) for d in (items or [])]

    async def aremove(self, entry_id: int) -> None:
        """Async version of remove()."""
        self._mon._assert_initialized()
        await _t.send_async_no_body(
            self._url(entry_id), self._mon._headers,
            self._mon._timeout, self._mon._retries, method="DELETE",
        )

    async def acheck(self, *, type: str, value: str) -> Dict[str, Any]:
        """Async version of check()."""
        self._mon._assert_initialized()
        data = await _t.send_async(
            self._check_url(), self._mon._headers,
            {"type": type, "value": value},
            self._mon._timeout, self._mon._retries,
        )
        return data if isinstance(data, dict) else {"blacklisted": False}


# ── Singleton ─────────────────────────────────────────────────────────────────
monitor = _Monitor()
