# retoor <retoor@molodetz.nl>

import fcntl
import json
import logging
import os
import random
from datetime import datetime
from pathlib import Path
from typing import Optional

from devplacepy.services.bot.config import (
    ARTICLE_TTL_DAYS,
    MAX_BOTS_PER_ARTICLE,
    persona_article_score,
)

logger = logging.getLogger(__name__)


class ArticleRegistry:
    def __init__(
        self,
        path: Path,
        max_per_article: int = MAX_BOTS_PER_ARTICLE,
        ttl_days: int = ARTICLE_TTL_DAYS,
    ):
        self.path = path
        self.max_per_article = max(1, max_per_article)
        self.ttl_days = max(1, ttl_days)

    @staticmethod
    def _normalize(meta: dict) -> dict:
        if "holders" in meta and isinstance(meta["holders"], list):
            return meta
        bot = meta.get("bot", "")
        time = meta.get("time", "")
        return {
            "holders": [{"bot": bot, "category": "", "time": time}],
            "time": time,
        }

    def _lock(self) -> tuple:
        self.path.parent.mkdir(parents=True, exist_ok=True)
        try:
            fd = os.open(str(self.path), os.O_RDWR | os.O_CREAT)
            fcntl.flock(fd, fcntl.LOCK_EX)
            data = {}
            if self.path.stat().st_size > 0:
                chunks = []
                while True:
                    chunk = os.read(fd, 65536)
                    if not chunk:
                        break
                    chunks.append(chunk)
                raw = b"".join(chunks).decode(errors="replace")
                if raw.strip():
                    try:
                        data = json.loads(raw)
                    except json.JSONDecodeError:
                        data = {}
            if not isinstance(data, dict):
                data = {}
            now = datetime.now()
            kept: dict = {}
            for title, meta in data.items():
                if not isinstance(meta, dict):
                    continue
                meta = self._normalize(meta)
                ts = meta.get("time", "")
                try:
                    age_days = (
                        now - datetime.fromisoformat(ts)
                    ).total_seconds() / 86400
                except (ValueError, TypeError):
                    age_days = 0
                if age_days < self.ttl_days:
                    kept[title] = meta
            return fd, kept
        except Exception as e:
            logger.warning("Failed to lock article registry: %s", e)
            return None, {}

    @staticmethod
    def _unlock(fd) -> None:
        if fd is not None:
            try:
                fcntl.flock(fd, fcntl.LOCK_UN)
                os.close(fd)
            except Exception as e:
                logger.debug("Failed to unlock article registry: %s", e)

    def _admit(self, entry: Optional[dict], owner: str, category: str) -> bool:
        if entry is None:
            return True
        holders = entry.get("holders", [])
        if any(h.get("bot") == owner for h in holders):
            return False
        if len(holders) >= self.max_per_article:
            return False
        if category and any(h.get("category") == category for h in holders):
            return False
        return True

    @staticmethod
    def _hold(registry: dict, title: str, owner: str, category: str) -> None:
        now = datetime.now().isoformat()
        entry = registry.get(title)
        if entry is None:
            registry[title] = {
                "holders": [{"bot": owner, "category": category, "time": now}],
                "time": now,
            }
            return
        entry["holders"].append({"bot": owner, "category": category, "time": now})
        entry["time"] = now

    def reserve(self, title: str, owner: str, category: str = "") -> bool:
        fd, registry = self._lock()
        if fd is None:
            return True
        try:
            entry = registry.get(title)
            if not self._admit(entry, owner, category):
                return False
            self._hold(registry, title, owner, category)
            self.path.write_text(json.dumps(registry, indent=2, default=str))
            return True
        finally:
            self._unlock(fd)

    def reserve_unused(
        self,
        articles: list[dict],
        owner: str,
        clean,
        category: str = "",
        persona: str = "",
    ) -> Optional[dict]:
        if not articles:
            return None
        fd, registry = self._lock()
        if fd is None:
            return articles[0]
        try:
            ranked = sorted(
                articles,
                key=lambda a: persona_article_score(a, persona) + random.random(),
                reverse=True,
            )
            for a in ranked:
                title = clean(a.get("title", "")[:200])
                if not title:
                    continue
                entry = registry.get(title)
                if not self._admit(entry, owner, category):
                    continue
                self._hold(registry, title, owner, category)
                self.path.write_text(json.dumps(registry, indent=2, default=str))
                return a
            return None
        finally:
            self._unlock(fd)
