"""Log service for submitting execution logs to Orchestrator."""

from __future__ import annotations

import asyncio
import platform
import re
import uuid
from collections import deque
from datetime import datetime
from pathlib import Path

from uipath.robot.infra import get_logger
from uipath.robot.models import LogEntry, LogLevel
from uipath.robot.services.orchestrator import OrchestratorService

# Regex pattern for log format: [timestamp][level] message
LOG_LINE_PATTERN = re.compile(r"^\[([^\]]+)\]\[([^\]]+)\]\s*(.*)$")

LOG_LEVEL_MAP: dict[str, LogLevel] = {
    "info": LogLevel.INFO,
    "warn": LogLevel.WARN,
    "error": LogLevel.ERROR,
}

FLUSH_INTERVAL_MS = 500
FLUSH_TIMEOUT_S = 15
LOGS_BATCH_SIZE = 50


class LogsSubmitterService:
    """Watches a log file and submits new entries to Orchestrator with batching."""

    def __init__(
        self,
        log_path: Path,
        orchestrator: OrchestratorService,
        robot_key: str,
        license_key: str,
        job_id: str,
        process_name: str,
        process_version: str,
        folder_id: int,
        robot_name: str,
        machine_id: int,
    ):
        """Initialize the log submitter.

        Args:
            log_path: Path to the execution.log file
            orchestrator: OrchestratorService instance
            robot_key: Robot key for authentication
            license_key: Robot license key for authentication
            job_id: Job ID for log entries
            process_name: Process name for log entries
            process_version: Process version for log entries
            folder_id: Organization unit/folder ID for log entries
            robot_name: Robot name for log entries
            machine_id: Machine ID for log entries
        """
        self.log_path = log_path
        self.orchestrator = orchestrator
        self.robot_key = robot_key
        self.license_key = license_key
        self.job_id = job_id
        self.process_name = process_name
        self.process_version = process_version
        self.folder_id = folder_id
        self.robot_name = robot_name
        self.machine_id = machine_id

        self._lines_read = 0
        self._stop_event = asyncio.Event()
        self._task: asyncio.Task[None] | None = None
        self._machine_name = platform.node()
        self._logger = get_logger()

        # In-memory queue for pending logs
        self._pending_logs: deque[LogEntry] = deque()
        self._is_initialized = False

    def _parse_log_line(self, line: str) -> LogEntry | None:
        """Parse a log line into a LogEntry.

        Args:
            line: Raw log line in format [timestamp][level] message

        Returns:
            LogEntry if parsing successful, None otherwise
        """
        match = LOG_LINE_PATTERN.match(line.strip())
        if not match:
            return None

        timestamp, level_str, message = match.groups()
        level = LOG_LEVEL_MAP.get(level_str.lower(), LogLevel.INFO)

        iso_timestamp = self._convert_timestamp_to_iso(timestamp)

        return LogEntry(
            message=message,
            level=level,
            timestamp=iso_timestamp,
            organization_unit_id=self.folder_id,
            process_name=self.process_name,
            process_version=self.process_version,
            file_name="Main",
            job_id=self.job_id,
            robot_name=self.robot_name,
            machine_id=self.machine_id,
            machine_name=self._machine_name,
            fingerprint=str(uuid.uuid4()),
        )

    def _convert_timestamp_to_iso(self, timestamp: str) -> str:
        """Convert log timestamp to ISO 8601 format.

        Args:
            timestamp: Timestamp in format "2026-01-14 18:41:49,207"

        Returns:
            ISO 8601 formatted timestamp with timezone
        """
        try:
            dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S,%f")
            dt = dt.astimezone()
            return dt.isoformat()
        except ValueError:
            return datetime.now().astimezone().isoformat()

    def _enqueue_from_file(self) -> None:
        """Read new lines from log file and enqueue them."""
        if not self.log_path.exists():
            return

        try:
            with open(self.log_path, "r", encoding="utf-8") as f:
                lines = f.readlines()
                new_lines = lines[self._lines_read :]
                self._lines_read = len(lines)

                for line in new_lines:
                    entry = self._parse_log_line(line)
                    if entry:
                        self._pending_logs.append(entry)
        except Exception as e:
            self._logger.debug(f"Error reading log file: {e}")

    def _take_batch(self, max_size: int = LOGS_BATCH_SIZE) -> list[LogEntry]:
        """Take up to max_size entries from the queue.

        Args:
            max_size: Maximum number of entries to take

        Returns:
            List of log entries (may be empty)
        """
        batch: list[LogEntry] = []
        while self._pending_logs and len(batch) < max_size:
            try:
                batch.append(self._pending_logs.popleft())
            except IndexError:
                break
        return batch

    async def _submit_batch(self) -> bool:
        """Submit a batch of logs to Orchestrator.

        Returns:
            True if submission successful or queue empty, False on error
        """
        batch = self._take_batch()
        if not batch:
            return True

        try:
            success = await self.orchestrator.submit_logs(
                batch, self.robot_key, self.license_key
            )
            if not success:
                # Re-queue failed batch at the front for retry
                for entry in reversed(batch):
                    self._pending_logs.appendleft(entry)
                return False
            return True
        except Exception as e:
            self._logger.error(f"Error submitting logs: {e}")
            # Re-queue on error
            for entry in reversed(batch):
                self._pending_logs.appendleft(entry)
            return False

    async def _sync_pending_items(self, draining: bool = False) -> None:
        """Submit all pending items in batches.

        Args:
            draining: If True, keep submitting until queue is empty
        """
        while self._pending_logs:
            success = await self._submit_batch()
            if not success and not draining:
                break
            if not draining:
                break

    async def _run_loop(self) -> None:
        """Main loop that reads logs and submits them in batches."""
        interval_s = FLUSH_INTERVAL_MS / 1000.0

        try:
            while not self._stop_event.is_set():
                self._enqueue_from_file()

                await self._sync_pending_items(draining=False)

                try:
                    await asyncio.wait_for(self._stop_event.wait(), timeout=interval_s)
                except asyncio.TimeoutError:
                    pass

        except asyncio.CancelledError:
            self._logger.debug("Log submitter cancelled")
        except Exception as e:
            self._logger.error(f"Error in log submitter loop: {e}")

        await self._drain()

    async def _drain(self) -> None:
        """Drain all remaining logs with timeout."""
        self._enqueue_from_file()

        count = len(self._pending_logs)
        if count == 0:
            return

        self._logger.debug(f"Draining {count} pending log entries")

        try:
            await asyncio.wait_for(
                self._sync_pending_items(draining=True),
                timeout=FLUSH_TIMEOUT_S,
            )
        except asyncio.TimeoutError:
            remaining = len(self._pending_logs)
            self._logger.warning(
                f"Drain timeout after {FLUSH_TIMEOUT_S}s, {remaining} logs not submitted"
            )
        except Exception as e:
            self._logger.error(f"Error during drain: {e}")

        self._logger.debug("Log submitter drain completed")

    def start(self) -> None:
        """Start the log submitter task."""
        if self._is_initialized:
            return

        self._stop_event.clear()
        self._task = asyncio.create_task(self._run_loop())
        self._is_initialized = True

    async def stop(self) -> None:
        """Stop the log submitter and drain remaining logs."""
        if not self._is_initialized:
            return

        self._stop_event.set()

        if self._task:
            try:
                await self._task
            except Exception as e:
                self._logger.error(f"Error while stopping log submitter: {e}")
            finally:
                self._task = None

        self._is_initialized = False
