#!/usr/bin/env python3
from __future__ import annotations

import json
import re
import uuid
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

try:
    import fcntl
except ImportError:  # pragma: no cover
    fcntl = None

from cheapclaw_channel_base import ChannelAdapter
from cheapclaw_runtime_state import atomic_write_text, load_json
from tool_runtime_helpers import list_peer_bots, now_iso


class LocalWebAdapter(ChannelAdapter):
    name = "localweb"
    MENTION_PATTERN = re.compile(r"@([A-Za-z0-9_.-]{1,80})")

    def __init__(self, config: Dict[str, Any], service: Any, *, logger=None):
        super().__init__(config, service, logger=logger)
        self.bot_id = str(config.get("bot_id") or service.bot_id).strip() or service.bot_id
        self.display_name = str(config.get("display_name") or service.bot_display_name or self.bot_id).strip() or self.bot_id
        self.require_mention_in_group = bool(config.get("require_mention_in_group", True))
        self.shared_root = self._resolve_shared_root()
        self.shared_root.mkdir(parents=True, exist_ok=True)
        self.events_path = self.shared_root / "events.jsonl"
        self.events_lock_path = self.shared_root / "events.lock"
        self.conversations_path = self.shared_root / "conversations.json"
        self._ensure_storage()

    def _resolve_shared_root(self) -> Path:
        if self.service.fleet_config_path:
            fleet_path = Path(self.service.fleet_config_path).expanduser().resolve()
            return fleet_path.parent / "localweb"
        return self.service.paths.user_data_root / "cheapclaw" / "localweb"

    def _ensure_storage(self) -> None:
        if not self.conversations_path.exists():
            atomic_write_text(self.conversations_path, json.dumps({"version": 1, "conversations": {}}, ensure_ascii=False, indent=2))
        self.events_path.touch(exist_ok=True)
        self.events_lock_path.touch(exist_ok=True)

    def _load_conversations_payload(self) -> Dict[str, Any]:
        payload = load_json(self.conversations_path, {"version": 1, "conversations": {}})
        if not isinstance(payload, dict):
            payload = {"version": 1, "conversations": {}}
        payload.setdefault("version", 1)
        conversations = payload.get("conversations")
        if not isinstance(conversations, dict):
            conversations = {}
        normalized: Dict[str, Dict[str, Any]] = {}
        for raw_id, item in conversations.items():
            conversation_id = str(raw_id or "").strip()
            if not conversation_id or not isinstance(item, dict):
                continue
            participant_bots = []
            for bot_id in item.get("participant_bots", []):
                bot_text = str(bot_id or "").strip()
                if bot_text and bot_text not in participant_bots:
                    participant_bots.append(bot_text)
            normalized[conversation_id] = {
                "conversation_id": conversation_id,
                "display_name": str(item.get("display_name") or conversation_id).strip() or conversation_id,
                "conversation_type": "group" if str(item.get("conversation_type") or "group").strip() == "group" else "person",
                "participant_bots": participant_bots,
                "require_mention": bool(item.get("require_mention", self.require_mention_in_group)),
                "created_at": str(item.get("created_at") or now_iso()),
                "updated_at": str(item.get("updated_at") or now_iso()),
                "created_by": str(item.get("created_by") or "system"),
            }
        payload["conversations"] = normalized
        return payload

    def _save_conversations_payload(self, payload: Dict[str, Any]) -> Dict[str, Any]:
        atomic_write_text(self.conversations_path, json.dumps(payload, ensure_ascii=False, indent=2))
        return payload

    def list_conversations(self) -> List[Dict[str, Any]]:
        payload = self._load_conversations_payload()
        items = [item for item in payload.get("conversations", {}).values() if isinstance(item, dict)]
        items.sort(key=lambda item: str(item.get("updated_at") or ""), reverse=True)
        return items

    def ensure_conversation_record(
        self,
        *,
        conversation_id: str,
        display_name: str = "",
        conversation_type: str = "",
        participant_bots: Optional[List[str]] = None,
        require_mention: Optional[bool] = None,
        created_by: str = "",
        include_self_participant: bool = True,
    ) -> Dict[str, Any]:
        conv_id = str(conversation_id or "").strip()
        if not conv_id:
            conv_id = f"lc_{uuid.uuid4().hex[:12]}"
        payload = self._load_conversations_payload()
        conversations = payload.setdefault("conversations", {})
        existing = conversations.get(conv_id) if isinstance(conversations.get(conv_id), dict) else {}
        if not existing:
            existing = {
                "conversation_id": conv_id,
                "display_name": conv_id,
                "conversation_type": "group",
                "participant_bots": [],
                "require_mention": self.require_mention_in_group,
                "created_at": now_iso(),
                "updated_at": now_iso(),
                "created_by": created_by or "system",
            }
        if display_name.strip():
            existing["display_name"] = display_name.strip()
        if conversation_type.strip() in {"group", "person"}:
            existing["conversation_type"] = conversation_type.strip()
        if require_mention is not None:
            existing["require_mention"] = bool(require_mention)
        members = list(existing.get("participant_bots") or [])
        if participant_bots is not None:
            members = []
            for bot_id in participant_bots:
                bot_text = str(bot_id or "").strip()
                if bot_text and bot_text not in members:
                    members.append(bot_text)
        if include_self_participant and self.bot_id not in members:
            members.append(self.bot_id)
        existing["participant_bots"] = members
        existing["updated_at"] = now_iso()
        conversations[conv_id] = existing
        self._save_conversations_payload(payload)
        return existing

    def _known_bot_id_map(self) -> Dict[str, str]:
        items = list_peer_bots(current_bot_id=self.service.bot_id, include_self=True, path=self.service.fleet_config_path)
        if not items:
            items = [{"bot_id": self.service.bot_id, "display_name": self.service.bot_display_name}]
        mapping: Dict[str, str] = {}
        for item in items:
            bot_id = str(item.get("bot_id") or "").strip()
            display_name = str(item.get("display_name") or "").strip()
            if bot_id:
                mapping[bot_id.lower()] = bot_id
            if display_name:
                mapping[display_name.lower()] = bot_id or display_name
        return mapping

    def parse_mentions(self, text: str) -> List[str]:
        bot_map = self._known_bot_id_map()
        mentions: List[str] = []
        for token in self.MENTION_PATTERN.findall(str(text or "")):
            key = str(token or "").strip().lower()
            if not key:
                continue
            resolved = bot_map.get(key) or str(token or "").strip()
            if resolved and resolved not in mentions:
                mentions.append(resolved)
        return mentions

    def _append_event_line(self, payload: Dict[str, Any]) -> None:
        lock_file = open(self.events_lock_path, "a+", encoding="utf-8")
        try:
            if fcntl is not None:
                fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
            with open(self.events_path, "a", encoding="utf-8") as fh:
                fh.write(json.dumps(payload, ensure_ascii=False) + "\n")
        finally:
            if fcntl is not None:
                fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
            lock_file.close()

    def emit_event(
        self,
        *,
        conversation_id: str,
        message: str,
        sender_id: str,
        sender_name: str,
        sender_type: str,
        attachments: Optional[List[Dict[str, Any]]] = None,
        display_name: str = "",
        participant_bots: Optional[List[str]] = None,
        require_mention: Optional[bool] = None,
    ) -> Dict[str, Any]:
        text = str(message or "").strip()
        if not text and not (attachments or []):
            return {"status": "error", "error": "localweb message or attachments are required"}
        mentions = self.parse_mentions(text)
        merged_participants = list(participant_bots or [])
        for mention in mentions:
            if mention not in merged_participants:
                merged_participants.append(mention)
        conv = self.ensure_conversation_record(
            conversation_id=conversation_id,
            display_name=display_name,
            conversation_type="group" if len(merged_participants) > 1 else "person",
            participant_bots=merged_participants if merged_participants else None,
            require_mention=require_mention,
            created_by=sender_id,
        )
        event_payload = {
            "event_id": f"lwevt_{uuid.uuid4().hex[:12]}",
            "channel": "localweb",
            "conversation_id": str(conv.get("conversation_id") or ""),
            "conversation_type": str(conv.get("conversation_type") or "group"),
            "display_name": str(conv.get("display_name") or conv.get("conversation_id") or ""),
            "participant_bots": list(conv.get("participant_bots") or []),
            "require_mention": bool(conv.get("require_mention", self.require_mention_in_group)),
            "sender_id": str(sender_id or ""),
            "sender_name": str(sender_name or sender_id or ""),
            "sender_type": str(sender_type or "user"),
            "message_text": text,
            "attachments": attachments or [],
            "mentions": mentions,
            "timestamp": now_iso(),
        }
        self._append_event_line(event_payload)
        return {"status": "success", "event": event_payload}

    def _load_new_bus_events(self) -> List[Tuple[int, Dict[str, Any]]]:
        state = self.service.load_runtime_state()
        offsets = state.setdefault("localweb_offsets", {})
        line_offset = int(offsets.get("line") or 0)
        lines = self.events_path.read_text(encoding="utf-8", errors="ignore").splitlines()
        new_events: List[Tuple[int, Dict[str, Any]]] = []
        next_offset = line_offset
        total_lines = len(lines)
        for idx, line in enumerate(lines[line_offset:], start=line_offset + 1):
            if not line.strip():
                next_offset = idx
                continue
            try:
                payload = json.loads(line)
            except Exception:
                if idx == total_lines:
                    break
                next_offset = idx
                continue
            if isinstance(payload, dict):
                new_events.append((idx, payload))
            next_offset = idx
        offsets["line"] = next_offset
        state["localweb_offsets"] = offsets
        self.service.save_runtime_state(state)
        return new_events

    def _should_deliver(self, payload: Dict[str, Any]) -> Tuple[bool, bool, Dict[str, Any]]:
        conversation_id = str(payload.get("conversation_id") or "").strip()
        if not conversation_id:
            return False, False, {}
        conv = self.ensure_conversation_record(
            conversation_id=conversation_id,
            display_name=str(payload.get("display_name") or ""),
            conversation_type=str(payload.get("conversation_type") or ""),
            participant_bots=list(payload.get("participant_bots") or []),
            require_mention=payload.get("require_mention"),
            include_self_participant=False,
        )
        sender_type = str(payload.get("sender_type") or "user").strip().lower() or "user"
        sender_id = str(payload.get("sender_id") or "").strip()
        mentions = [str(item).strip() for item in payload.get("mentions", []) if str(item).strip()]
        mention_to_bot = self.bot_id in mentions
        if sender_type == "bot":
            if sender_id == self.bot_id:
                return False, False, conv
            if not mention_to_bot:
                return False, False, conv
        if mentions and not mention_to_bot:
            return False, False, conv
        participants = [str(item).strip() for item in conv.get("participant_bots", []) if str(item).strip()]
        if participants and self.bot_id not in participants and not mention_to_bot:
            return False, mention_to_bot, conv
        conversation_type = str(conv.get("conversation_type") or "group")
        require_mention = bool(conv.get("require_mention", self.require_mention_in_group))
        is_group = conversation_type == "group"
        if is_group and require_mention and not mention_to_bot:
            return False, mention_to_bot, conv
        return True, mention_to_bot or (not (is_group and require_mention)), conv

    def poll_events(self) -> List[Dict[str, Any]]:
        events: List[Dict[str, Any]] = []
        for index, payload in self._load_new_bus_events():
            deliver, is_mention_to_bot, conv = self._should_deliver(payload)
            if not deliver:
                continue
            sender_name = str(payload.get("sender_name") or payload.get("sender_id") or "").strip()
            sender_id = str(payload.get("sender_id") or "").strip()
            event_id = str(payload.get("event_id") or f"lwevt_{index}")
            events.append(
                {
                    "event_id": f"{event_id}:{self.bot_id}",
                    "channel": "localweb",
                    "conversation_id": str(conv.get("conversation_id") or payload.get("conversation_id") or ""),
                    "conversation_type": str(conv.get("conversation_type") or payload.get("conversation_type") or "group"),
                    "display_name": str(conv.get("display_name") or payload.get("display_name") or payload.get("conversation_id") or ""),
                    "sender_id": sender_id,
                    "sender_name": sender_name or "localweb_user",
                    "message_id": event_id,
                    "message_text": str(payload.get("message_text") or ""),
                    "attachments": payload.get("attachments") or [],
                    "timestamp": str(payload.get("timestamp") or now_iso()),
                    "is_mention_to_bot": bool(is_mention_to_bot),
                    "require_mention": bool(conv.get("require_mention", self.require_mention_in_group)),
                }
            )
        return events

    def send_message(self, conversation_id: str, message: str, attachments: Optional[List[Dict[str, Any]]] = None) -> Tuple[bool, str]:
        result = self.emit_event(
            conversation_id=conversation_id,
            message=message,
            sender_id=self.service.bot_id,
            sender_name=self.service.bot_display_name,
            sender_type="bot",
            attachments=attachments or [],
        )
        if result.get("status") != "success":
            return False, str(result.get("error") or "localweb send failed")
        event = result.get("event") if isinstance(result.get("event"), dict) else {}
        return True, str(event.get("event_id") or "")
