# xlog/group/file.py
from __future__ import annotations

import json
import threading
from pathlib import Path
from typing import Optional

from xlog.event.base import EventLike
from xlog.group.base import BaseGroup


class FileGroup(BaseGroup):
    """
    DESC:
        File-based logging group that writes events to JSONL format.
        Each event is written as one JSON line. File name is {name}.log.

    Params:
        path: str | Path, directory path where log file will be created.
        id: Optional[str] = None, unique identifier for the group.
        name: Optional[str] = None, name of the group (used for filename).
        encoding: str = "utf-8", file encoding.
        ensure_ascii: bool = False, whether to escape non-ASCII characters.
        store: bool = True, whether to store events in memory.
        async_: bool = False, whether to process events asynchronously.
        max_queue: int = 1000, maximum queue size for async processing.
        max_len: Optional[int] = 100_000, maximum events to store per stream.
        max_size_mb: float = 10, maximum log file size in MB.

    Examples:
        ```python
        from xlog.group.filegroup import FileGroup

        # Write logs to file
        group = FileGroup(path="/tmp/logs", name="app", async_=True)
        ```
    """

    def __init__(
        self,
        path: str | Path,
        id: Optional[str] = None,
        name: Optional[str] = None,
        encoding: str = "utf-8",
        ensure_ascii: bool = False,
        store: bool = True,
        async_: bool = False,
        max_queue: int = 1000,
        max_len: Optional[int] = 100_000,
        max_size_mb: float = 10,
    ):
        super().__init__(
            id=id,
            name=name,
            store=store,
            async_=async_,
            max_queue=max_queue,
            max_len=max_len,
        )
        self.path = self._resolve_path(path)
        self.file_path = self.path / f"{self.name}.log"
        self.encoding = encoding
        self.ensure_ascii = ensure_ascii
        self.max_size_mb = max_size_mb
        self.max_bytes = int(self.max_size_mb * 1024 * 1024)
        self._lock = threading.RLock()
        self._cursor = open(
            self.file_path,
            "a",
            encoding=self.encoding,
        )

    def _resolve_path(
        self,
        value: str | Path,
    ) -> Path:
        path = Path(value).expanduser().resolve()
        path.mkdir(parents=True, exist_ok=True)
        return path

    # ---------- overridable hooks ----------
    def _consume(
        self,
        stream: str,
        event: EventLike,
    ) -> None:
        payload = event.to_dict()
        line = json.dumps(
            payload,
            ensure_ascii=self.ensure_ascii,
            sort_keys=True,
            indent=2,
        )
        with self._lock:
            self._cursor.write(line + "\n")
            self._cursor.flush()
            self._enforce_size_limit()

    def _enforce_size_limit(self) -> None:
        if self.max_bytes <= 0:
            return
        if not self.file_path.exists():
            return
        if self.file_path.stat().st_size <= self.max_bytes:
            return

        events = self._read_events_from_file()
        if not events:
            # Fallback for corrupted content: keep file bounded.
            with open(self.file_path, "w", encoding=self.encoding) as f:
                f.write("")
            self._reopen_cursor()
            return

        serialized = [
            json.dumps(
                item,
                ensure_ascii=self.ensure_ascii,
                sort_keys=True,
                indent=2,
            )
            + "\n"
            for item in events
        ]
        total_bytes = sum(len(item.encode(self.encoding)) for item in serialized)

        while serialized and total_bytes > self.max_bytes:
            removed = serialized.pop(0)
            total_bytes -= len(removed.encode(self.encoding))

        with open(self.file_path, "w", encoding=self.encoding) as f:
            f.write("".join(serialized))
        self._reopen_cursor()

    def _read_events_from_file(self) -> list[dict]:
        content = self.file_path.read_text(encoding=self.encoding)
        decoder = json.JSONDecoder()
        events: list[dict] = []
        idx = 0
        size = len(content)

        while idx < size:
            while idx < size and content[idx].isspace():
                idx += 1
            if idx >= size:
                break
            try:
                parsed, end = decoder.raw_decode(content, idx)
            except json.JSONDecodeError:
                return []
            if isinstance(parsed, dict):
                events.append(parsed)
            idx = end

        return events

    def _reopen_cursor(self) -> None:
        if not self._cursor.closed:
            self._cursor.close()
        self._cursor = open(
            self.file_path,
            "a",
            encoding=self.encoding,
        )

    def flush(self) -> None:
        super().flush()
        with self._lock:
            if not self._cursor.closed:
                self._cursor.flush()

    def _on_close(self) -> None:
        with self._lock:
            try:
                if not self._cursor.closed:
                    self._cursor.flush()
                    self._cursor.close()
            finally:
                pass
