"""
Receipt reader utilities.

Provides lightweight helpers for inspecting recently written receipts without
requiring the full index API. The implementation intentionally avoids heavy
dependencies so tests can rely on it in isolated tmp directories.
"""

from __future__ import annotations

import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Iterable, List, Optional, Sequence, Set

from config.paths import DATA_ROOT
from receipts.models import BaseReceipt, load_receipt
from receipts import index as receipt_index


def _coerce_timestamp(value: object) -> Optional[datetime]:
    """Best-effort conversion of ledger ts fields into aware datetimes."""
    if isinstance(value, datetime):
        return value.astimezone(timezone.utc)
    if isinstance(value, (int, float)):
        # Heuristic: large numbers are milliseconds since epoch.
        seconds = float(value) / 1000.0 if value > 10**11 else float(value)
        try:
            return datetime.fromtimestamp(seconds, tz=timezone.utc)
        except (OverflowError, OSError, ValueError):
            return None
    if isinstance(value, str):
        token = value.strip()
        if not token:
            return None
        if token.endswith("Z"):
            token = token[:-1] + "+00:00"
        try:
            parsed = datetime.fromisoformat(token)
        except ValueError:
            return None
        return parsed.astimezone(timezone.utc) if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
    return None


def _iter_inbox_payloads(base: Path) -> Iterable[tuple[str, Path]]:
    """Yield candidate receipt JSON files from the inbox tree."""
    inbox_root = base / "receipts" / "inbox"
    if not inbox_root.exists():
        return
    for path in sorted(inbox_root.rglob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
        receipt_id = path.stem
        yield receipt_id, path


def read_recent_receipts(
    *,
    days: int = 1,
    limit: Optional[int] = None,
    receipt_types: Optional[Sequence[str]] = None,
    root: Optional[Path] = None,
) -> List[BaseReceipt]:
    """
    Load recent receipts from the local inbox/ledger directories.

    Args:
        days: Lookback window (default: 1 day).
        limit: Optional maximum number of receipts to return.
        receipt_types: Optional whitelist of receipt types.
        root: Override for DATA_ROOT (useful in tests).

    Returns:
        List of BaseReceipt instances sorted by most recent first.
    """
    base = Path(root or DATA_ROOT)
    cutoff = datetime.now(timezone.utc) - timedelta(days=max(days, 0))
    type_filter: Optional[Set[str]] = (
        {t.strip().lower() for t in receipt_types if t} if receipt_types else None
    )
    results: List[BaseReceipt] = []
    seen_ids: Set[str] = set()

    def _should_include(receipt: BaseReceipt) -> bool:
        if type_filter and receipt.type.lower() not in type_filter:
            return False
        receipt_ts = getattr(receipt, "ts", None)
        if isinstance(receipt_ts, datetime):
            if receipt_ts.tzinfo is None:
                receipt_ts = receipt_ts.replace(tzinfo=timezone.utc)
            if receipt_ts < cutoff:
                return False
        return True

    # Prefer ledger index when available for efficiency.
    try:
        ledger_entries = list(receipt_index._iter_ledger_entries(root=root))  # type: ignore[attr-defined]
    except Exception:
        ledger_entries = []

    for entry in ledger_entries:
        receipt_id = str(entry.get("receipt_id") or "")
        if not receipt_id or receipt_id in seen_ids:
            continue
        ts_value = _coerce_timestamp(entry.get("ts"))
        if ts_value and ts_value < cutoff:
            continue
        try:
            payload = receipt_index.load_receipt_from_index(receipt_id, root=root)
        except FileNotFoundError:
            continue
        except Exception:
            continue
        try:
            receipt = load_receipt(payload)
        except Exception:
            continue
        if not _should_include(receipt):
            continue
        results.append(receipt)
        seen_ids.add(receipt_id)
        if limit is not None and len(results) >= limit:
            break

    # Fall back to scanning inbox files for scenarios where the index
    # has not been initialised yet (common in tests).
    if limit is None or len(results) < limit:
        for receipt_id, path in _iter_inbox_payloads(base):
            if receipt_id in seen_ids:
                continue
            try:
                payload = json.loads(path.read_text(encoding="utf-8"))
            except (OSError, json.JSONDecodeError):
                continue
            try:
                receipt = load_receipt(payload)
            except Exception:
                continue
            if not _should_include(receipt):
                continue
            results.append(receipt)
            seen_ids.add(receipt_id)
            if limit is not None and len(results) >= limit:
                break

    results.sort(key=lambda r: getattr(r, "ts", datetime.min.replace(tzinfo=timezone.utc)), reverse=True)
    return results


__all__ = ["read_recent_receipts"]
