"""
QueueService facade — composes the queue subsystems into the existing public API.

All callers continue to import ``QueueService`` and ``QueueError`` from
``actor.queue`` with no changes.
"""
from __future__ import annotations

import json
import logging
import time
from datetime import datetime, timezone
from typing import Any, Callable, Dict, List, Optional

from actor.queue.connection import QueueError, RedisConnection
from actor.queue.consumer import TaskConsumer
from actor.queue.dedup import DeduplicationTracker
from actor.queue.publisher import ResultPublisher
from actor.queue.config_listener import ConfigListener

logger = logging.getLogger(__name__)


class QueueService:
    """Facade composing queue subsystems.  Preserves existing API."""

    def __init__(
        self,
        agent_key: str,
        instance_id: str,
        consumer_group: str = "workers",
        redis_client: Any = None,
        redis_url: Optional[str] = None,
        redis_password: Optional[str] = None,
        reconnect_delay: int = 1,
        max_reconnect_delay: int = 30,
        pending_min_idle_ms: int = 30000,
    ) -> None:
        self.agent_key = agent_key
        self.instance_id = instance_id
        self.consumer_group = consumer_group
        self.tasks_stream = f"agent:{agent_key}:tasks"
        self.results_stream = f"agent:{agent_key}:results"
        self.state_key = f"agent:{agent_key}:state"
        self.pending_min_idle_ms = pending_min_idle_ms

        # Subsystems
        self._conn = RedisConnection(
            redis_client=redis_client,
            redis_url=redis_url,
            redis_password=redis_password,
            reconnect_delay=reconnect_delay,
            max_reconnect_delay=max_reconnect_delay,
        )
        self._publisher = ResultPublisher(self._conn, instance_id, self.results_stream)
        self._dedup = DeduplicationTracker(self._conn, self.state_key)
        self._consumer = TaskConsumer(
            self._conn, self._dedup, self._publisher,
            self.tasks_stream, consumer_group, instance_id, self.state_key,
        )
        self._config = ConfigListener(self._conn, self.results_stream, self.tasks_stream, instance_id)

        # Expose redis property for backward compatibility
        self.redis = self._conn.client

    # -- Consumer --------------------------------------------------------

    def initialize_consumer(self) -> None:
        self._consumer.initialize_consumer()

    def consume_tasks(
        self,
        callback: Callable[[Dict[str, Any]], Dict[str, Any]],
        block_ms: int = 5000,
        task_timeout: Optional[int] = 30,
        max_idle_iterations: Optional[int] = None,
        start_id: Optional[str] = None,
    ) -> None:
        self._consumer.consume_tasks(
            callback, block_ms=block_ms, task_timeout=task_timeout,
            max_idle_iterations=max_idle_iterations, start_id=start_id,
        )

    def stop(self) -> None:
        self._consumer.stop()

    # -- Publisher -------------------------------------------------------

    def publish_result(self, task_id: str, result: Dict[str, Any]) -> None:
        self._publisher.publish_result(task_id, result)

    def publish_error(self, task_id: str, error: str) -> None:
        self._publisher.publish_error(task_id, error)

    def publish_heartbeat(self, status: str, components: Dict[str, Any]) -> None:
        self._publisher.publish_heartbeat(status, components)

    def publish_report(self, report_type: str, data: Dict[str, Any]) -> None:
        self._publisher.publish_report(report_type, data)

    def publish_config_request(self) -> None:
        self._publisher.publish_config_request()

    def publish_config_applied(self, version: Optional[int]) -> None:
        self._publisher.publish_config_applied(version)

    def publish_instance_registration(self, instance_name: Optional[str], version: str, metadata: Dict[str, Any]) -> None:
        self._publisher.publish_instance_registration(instance_name, version, metadata)

    def publish_backlog_report(self) -> None:
        data = self.audit_backlog()
        self._publisher.publish_backlog_report(data)

    # -- Config listener -------------------------------------------------

    @property
    def last_config_id(self) -> Optional[str]:
        return self._config.last_config_id

    @last_config_id.setter
    def last_config_id(self, value: Optional[str]) -> None:
        self._config.last_config_id = value

    @property
    def last_registration_id(self) -> Optional[str]:
        return self._config.last_registration_id

    def capture_results_cursor(self) -> str:
        return self._config.capture_results_cursor()

    def wait_for_registration_ack(self, instance_id: str, timeout: int = 30, accept_config_as_ack: bool = True, start_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
        return self._config.wait_for_registration_ack(instance_id, timeout, accept_config_as_ack, start_id=start_id)

    def wait_for_config(self, timeout: int = 30) -> Dict[str, Any]:
        return self._config.wait_for_config(timeout)

    def poll_config_updates(self, block_ms: int = 1000, count: int = 5) -> List[Dict[str, Any]]:
        return self._config.poll_config_updates(block_ms, count)

    @staticmethod
    def _parse_config_payload(msg: "Dict[str, Any] | str") -> Dict[str, Any]:
        from actor.runtime.config_store import parse_config_payload
        return parse_config_payload(msg)

    # -- Dedup -----------------------------------------------------------

    def audit_backlog(self) -> Dict[str, Any]:
        return self._dedup.audit_backlog(
            self.tasks_stream, self.consumer_group, self.instance_id, self.pending_min_idle_ms,
        )

    # -- SaaS-side helpers -----------------------------------------------

    def publish_task(self, task_id: str, task_type: str, payload: Dict[str, Any]) -> None:
        self._conn.client.xadd(
            self.tasks_stream,
            {
                "type": "task",
                "task_id": task_id,
                "task_type": task_type,
                "payload": json.dumps(payload),
                "timestamp": datetime.now(timezone.utc).isoformat(),
            },
        )
        logger.info("Published task task_id=%s task_type=%s", task_id, task_type)

    def wait_for_result(self, task_id: str, timeout: int = 60) -> Dict[str, Any]:
        start = time.time()
        last_id = "$"
        while time.time() - start < timeout:
            messages = self._conn.client.xread({self.results_stream: last_id}, count=10, block=1000)
            for _, msg_list in messages:
                for msg_id, msg_data in msg_list:
                    if msg_data.get("task_id") == task_id and msg_data.get("type") == "task_result":
                        status = msg_data.get("status")
                        if status == "success":
                            try:
                                return json.loads(msg_data.get("result", "{}"))
                            except Exception:
                                return msg_data
                        raise QueueError(msg_data.get("error", "Unknown error"))
                    last_id = msg_id
            if time.time() - start >= timeout:
                break
        logger.error("Timed out waiting for result task_id=%s after %ss", task_id, timeout)
        raise TimeoutError(f"Task {task_id} timed out after {timeout}s")

    def consume_events(self, callback: Callable[[str, Dict[str, Any]], None], last_id: str = "$") -> None:
        while True:
            messages = self._conn.client.xread({self.results_stream: last_id}, count=100, block=5000)
            if not messages:
                continue
            for _, msg_list in messages:
                for msg_id, msg_data in msg_list:
                    event_type = msg_data.get("type")
                    callback(event_type, msg_data)
                    last_id = msg_id
                    logger.debug("Consumed event type=%s id=%s", event_type, msg_id)

    # -- Maintenance -----------------------------------------------------

    def trim_streams(self, max_len: int = 10000) -> None:
        self._conn.client.xtrim(self.tasks_stream, maxlen=max_len, approximate=True)
        self._conn.client.xtrim(self.results_stream, maxlen=max_len, approximate=True)
        logger.info("Trimmed streams to max_len=%s", max_len)
