import time
from contextlib import suppress
from typing import Any

from orjson import JSONDecodeError

import frappe
from frappe.utils import get_request_session
from frappe.utils.caching import site_cache
from frappe.utils.frappecloud import on_frappecloud

from .utils import anonymize_user, ensure_http, parse_interval, utc_iso


@frappe.whitelist()
@site_cache(ttl=60 * 60)
def is_enabled() -> bool:
	return bool(
		not frappe.conf.get("developer_mode", 0)
		and frappe.conf.get("pulse_api_key")
		and on_frappecloud()
		and frappe.get_system_settings("enable_telemetry")
	)


@frappe.whitelist()
def capture(
	event_name: str,
	site: str | None = None,
	app: str | None = None,
	user: str | None = None,
	captured_at: str | None = None,
	properties: dict[str, Any] | None = None,
	interval: int | str | None = None,
):
	if not is_enabled():
		return

	try:
		eq = EventQueue()
		eq.add(
			{
				"event_name": event_name,
				"captured_at": captured_at or utc_iso(),
				"app": app,
				"user": anonymize_user(user),
				"site": site or frappe.local.site,
				"properties": properties,
			},
			interval=interval,
		)
	except Exception as e:
		frappe.logger("pulse").error(f"pulse-client - capture failed: {e!s}")


@frappe.whitelist()
def bulk_capture(events: str | list[dict[str, Any]]):
	if not is_enabled():
		return

	if isinstance(events, str):
		events = frappe.parse_json(events)

	for event in events:
		capture(
			event.get("event_name"),
			site=event.get("site"),
			app=event.get("app"),
			user=event.get("user") or frappe.session.user,
			captured_at=event.get("captured_at"),
			properties=event.get("properties"),
			interval=event.get("interval"),
		)


def send_queued_events():
	if not is_enabled():
		return

	eq = EventQueue()
	eq.batch_process(post, batch_size=100, max_batches=10)


def post(events):
	session = _create_session()
	url = _get_ingest_url()
	data = frappe.as_json({"events": events})
	resp = session.post(url, data=data, timeout=15)
	if not (200 <= resp.status_code < 300):
		msg = f"pulse-client - post failed: {resp.status_code} {resp.text}"
		frappe.logger("pulse").error(msg)
		raise Exception(msg)
	return resp


def _create_session():
	api_key = frappe.conf.get("pulse_api_key")
	session = get_request_session()
	session.headers.update(
		{
			"Content-Type": "application/json",
			"X-Pulse-API-Key": api_key,
		}
	)
	return session


def _get_ingest_url():
	host = frappe.conf.get("pulse_host") or "https://pulse.m.frappe.cloud"
	host = ensure_http(host)
	host = host.rstrip("/")

	endpoint = frappe.conf.get("pulse_ingest_endpoint") or "/api/method/pulse.api.bulk_ingest"
	endpoint = endpoint.lstrip("/")

	return f"{host}/{endpoint}"


class EventQueue:
	def __init__(self):
		self.queue = "pulse-client:events"
		self.queue_size = 10000
		self.ratelimit_prefix = "pulse-client:last_sent:"

	@property
	def length(self):
		return frappe.cache.llen(self.queue)

	def add(self, event, interval=None):
		if self._is_ratelimited(event, interval):
			return

		self._queue_event(event)
		self._update_ratelimit(event, interval)

	def _is_ratelimited(self, event, interval):
		if not interval:
			return False

		interval_seconds = parse_interval(interval)
		event_key = self._get_event_key(event)
		last_sent_key = f"{self.ratelimit_prefix}{event_key}"
		last_sent = frappe.cache.get_value(last_sent_key)

		if last_sent and time.monotonic() - float(last_sent) < interval_seconds:
			return True

		return False

	def _get_event_key(self, event):
		return f"{event.get('event_name')}:{event.get('site')}:{event.get('app')}:{event.get('user')}"

	def _update_ratelimit(self, event, interval):
		if not interval:
			return
		event_key = self._get_event_key(event)
		last_sent_key = f"{self.ratelimit_prefix}{event_key}"
		frappe.cache.set_value(last_sent_key, time.monotonic())

	def _queue_event(self, event):
		frappe.cache.lpush(self.queue, frappe.as_json(event))
		frappe.cache.ltrim(self.queue, 0, self.queue_size - 1)

	def batch_process(self, fn, batch_size=100, max_batches=10, max_retries=3, backoff_seconds=1):
		pending_events = None
		retry_attempts = 0

		for _ in range(max_batches):
			events = pending_events or self.collect(batch_size)
			if not events:
				break

			try:
				fn(events)
				pending_events = None
				retry_attempts = 0
			except Exception as e:
				retry_attempts += 1
				if retry_attempts > max_retries:
					# Tried enough times, re-queue pending events and exit.
					frappe.logger("pulse").error(f"pulse-client - max retries reached: {e!s}")
					self._requeue_events(events)
					break

				pending_events = events
				time.sleep(backoff_seconds * (2 ** (retry_attempts - 1)))
				frappe.logger("pulse").error(f"pulse-client - retrying batch due to error: {e!s}")

	def collect(self, batch_size=100):
		events = []
		for _ in range(batch_size):
			event_json = frappe.cache.rpop(self.queue)
			if not event_json:
				break
			data = self._decode_event(event_json)
			if data:
				events.append(data)
		return events

	def _requeue_events(self, events):
		# Preserve original processing order (FIFO): we pop from right, so re-add in reverse.
		for event in reversed(events):
			frappe.cache.rpush(self.queue, frappe.as_json(event))
		frappe.cache.ltrim(self.queue, 0, self.queue_size - 1)

	def _decode_event(self, event_json):
		event_json = event_json.decode()
		with suppress(JSONDecodeError):
			return frappe.parse_json(event_json)

	def get_events(self, limit=20):
		events = []
		for _ in range(limit):
			event_json = frappe.cache.lindex(self.queue, _)
			if not event_json:
				break
			data = self._decode_event(event_json)
			if data:
				events.append(data)
		return events

	def get_last_sent_events(self, limit=20):
		events = []
		keys = frappe.cache.get_keys(f"{self.ratelimit_prefix}*")[:limit]
		for key in keys:
			last_sent = frappe.cache.get_value(key)
			event_key = key.replace(self.ratelimit_prefix, "")
			events.append(
				{
					"event_key": event_key,
					"last_sent": last_sent,
				}
			)
		return events


@frappe.whitelist()
def get_debug_info(
	fetch_events: int | str | bool | None = None, fetch_rate_limited_events: int | str | bool | None = None
):
	frappe.only_for("System Manager")

	info = frappe._dict()
	info.is_enabled = is_enabled()

	if info.is_enabled:
		eq = EventQueue()
		info.queued_event_count = eq.length

		if fetch_events:
			limit = int(fetch_events) if str(fetch_events).isdigit() else 20
			info.queued_events = eq.get_events(limit)

		if fetch_rate_limited_events:
			limit = int(fetch_rate_limited_events) if str(fetch_rate_limited_events).isdigit() else 20
			info.rate_limited_events = eq.get_last_sent_events(limit)

	return info
