"""Concrete Lark-backed Channel implementation."""

from __future__ import annotations

import json
import logging
import os

log = logging.getLogger(__name__)

from .channel import Channel, IncomingMessage
from .config import load_credentials, load_relay_config
from .lark import api as lark_api
from .lark import auth as lark_auth
from .lark.events import LarkEvent, LarkEventStream
from .relay_events import RelayEventStream
from .types import JsonObject


def _to_incoming(event: LarkEvent, token: str = "") -> IncomingMessage:
  text = event.text
  msg_type = event.msg_type

  # Enrich: download files and embed path in text
  if msg_type == "file" and event.file_key and event.message_id and token:
    try:
      path = lark_api.download_file(
        token, event.message_id, event.file_key, event.file_name)
      label = event.file_name or event.file_key
      text = f"[file: {path}] {label}" if not text else f"{text}\n[file: {path}]"
      log.info("Downloaded file: %s -> %s", label, path)
    except Exception as e:
      log.warning("File download failed: %s", e)
      text = f"[file download failed: {event.file_name or event.file_key}]"

  # Enrich: download images (pure image or inline in post)
  # Relay may send multiple keys comma-separated for post messages
  if event.image_key and event.message_id and token:
    for img_key in event.image_key.split(","):
      img_key = img_key.strip()
      if not img_key:
        continue
      try:
        path = lark_api.download_image(token, event.message_id, img_key)
        # Replace [image] placeholder with actual path, or append
        if "[image]" in text:
          text = text.replace("[image]", f"[image: {path}]", 1)
        else:
          text = f"{text}\n[image: {path}]" if text else f"[image: {path}]"
        log.info("Downloaded image: %s -> %s", img_key, path)
      except Exception as e:
        log.warning("Image download failed (%s): %s", img_key, e)

  # Enrich: fetch reply parent context
  if event.parent_id and token:
    try:
      parent = lark_api.get_message(token, event.parent_id)
      parent_text = _extract_message_text(parent)
      if parent_text:
        text = f"[replying to: {parent_text}]\n{text}"
    except Exception as e:
      log.warning("Failed to fetch parent message: %s", e)

  # Enrich: expand merge_forward (folder) messages
  if msg_type == "merge_forward" and event.message_id and token:
    try:
      text = _expand_merge_forward(token, event.message_id) or text
    except Exception as e:
      log.warning("Failed to expand merge_forward: %s", e)

  return IncomingMessage(
    event_type=event.event_type,
    chat_id=event.chat_id,
    chat_type=event.chat_type,
    sender_id=event.sender_id,
    message_id=event.message_id,
    msg_type=msg_type,
    text=text,
    mentions=list(event.mentions),
    image_key=event.image_key,
    file_key=event.file_key,
    file_name=event.file_name,
    parent_id=event.parent_id,
    create_time=event.create_time,
    action_value=dict(event.action_value),
    action_tag=event.action_tag,
    operator_id=event.operator_id,
    raw=dict(event.raw),
  )


def _extract_message_text(msg: JsonObject) -> str:
  """Extract readable text from a get_message API response item."""
  msg_type = msg.get("msg_type", "")
  body = msg.get("body", {})
  content_str = body.get("content", "{}") if isinstance(body, dict) else "{}"
  try:
    content = json.loads(content_str) if isinstance(content_str, str) else {}
    if msg_type == "text":
      return str(content.get("text", ""))
    if msg_type == "post":
      return _extract_post_text(content)
    if msg_type == "file":
      return f"[file: {content.get('file_name', '?')}]"
    if msg_type == "image":
      return "[image]"
  except (json.JSONDecodeError, TypeError) as e:
    log.debug("Malformed message content (type=%s): %s", msg_type, e)
  return f"[{msg_type}]" if msg_type else ""


def _extract_post_text(content: JsonObject) -> str:
  """Extract text from a post (rich text) message content."""
  post = content
  # Post content may be locale-keyed: {"zh_cn": {"title": ..., "content": [...]}}
  if not isinstance(content.get("content"), list):
    locale = next(iter(content), None)
    post = content.get(locale, {}) if locale else {}
    if not isinstance(post, dict):
      return "[post]"
  paragraphs = post.get("content", [])
  if not isinstance(paragraphs, list):
    return "[post]"
  parts: list[str] = []
  for para in paragraphs:
    if not isinstance(para, list):
      continue
    for elem in para:
      if not isinstance(elem, dict):
        continue
      if elem.get("text"):
        parts.append(str(elem["text"]))
      elif elem.get("tag") == "img":
        parts.append("[image]")
  title = post.get("title", "")
  text = "\n".join(parts)
  if title:
    text = f"{title}\n{text}"
  return text or "[post]"


def _expand_merge_forward(token: str, message_id: str) -> str:
  """Expand a merge_forward (folder) message into readable text."""
  msg = lark_api.get_message(token, message_id)
  body = msg.get("body", {})
  content_str = body.get("content", "{}") if isinstance(body, dict) else "{}"
  try:
    content = json.loads(content_str) if isinstance(content_str, str) else {}
  except (json.JSONDecodeError, TypeError):
    return "[folder message: could not parse]"

  # merge_forward content has sub messages inline or as IDs
  # Try to extract text from the content directly
  if isinstance(content, dict):
    # Some formats have "title" and "messages" or "message_id_list"
    title = content.get("title", "")
    parts: list[str] = []
    if title:
      parts.append(f"[folder: {title}]")

    # If content has inline text
    for key in ("text", "content"):
      if key in content and isinstance(content[key], str):
        parts.append(content[key])
        return "\n".join(parts)

  return f"[folder message]"


class LarkChannel(Channel):
  """Channel implementation backed by Lark IM APIs and event streams."""

  def __init__(self, chat_id: str):
    self.chat_id = chat_id
    self.credentials = load_credentials() or {}
    self._events: LarkEventStream | RelayEventStream | None = None

  @property
  def token(self) -> str:
    """Always return a fresh token (cached inside LarkAuth singleton)."""
    return lark_auth.get_token(
      self.credentials["app_id"], self.credentials["app_secret"])

  async def start(self) -> None:
    # Warm up the token cache
    _ = self.token
    relay_url, relay_api_key = load_relay_config()
    if relay_url:
      self._events = RelayEventStream(relay_url, relay_api_key, self.chat_id)
    else:
      self._events = LarkEventStream(
        self.credentials["app_id"], self.credentials["app_secret"])
    await self._events.connect()

  async def stop(self) -> None:
    if self._events is not None:
      await self._events.close()

  async def receive(self, timeout: float = 300) -> IncomingMessage | None:
    if self._events is None:
      raise RuntimeError("Channel not started")
    event = await self._events.next_message(timeout=timeout)
    if event is None:
      return None
    return _to_incoming(event, token=self.token)

  def push_back(self, message: IncomingMessage) -> None:
    if self._events is None:
      raise RuntimeError("Channel not started")
    event = LarkEvent(
      event_type=message.event_type,
      chat_id=message.chat_id,
      chat_type=message.chat_type,
      sender_id=message.sender_id,
      message_id=message.message_id,
      msg_type=message.msg_type,
      text=message.text,
      mentions=list(message.mentions),
      image_key=message.image_key,
      file_key=message.file_key,
      file_name=message.file_name,
      parent_id=message.parent_id,
      create_time=message.create_time,
      action_value=dict(message.action_value),
      action_tag=message.action_tag,
      operator_id=message.operator_id,
      raw=dict(message.raw),
    )
    self._events.push_back(event)

  def _retry_on_auth_error(self, fn, *args):
    """Call fn(token, *args); on auth error invalidate token and retry once."""
    try:
      return fn(self.token, *args)
    except RuntimeError as e:
      if "99991663" in str(e) or "99991668" in str(e):
        lark_auth.invalidate()
        return fn(self.token, *args)
      raise

  async def send_card(self, chat_id: str, card: JsonObject) -> str:
    return self._retry_on_auth_error(lark_api.send_card, chat_id, card)

  async def update_card(self, message_id: str, card: JsonObject) -> None:
    self._retry_on_auth_error(lark_api.update_card, message_id, card)

  async def send_text(self, chat_id: str, text: str) -> str:
    return self._retry_on_auth_error(lark_api.send_text, chat_id, text)

  async def download_image(self, message_id: str, image_key: str) -> str:
    return lark_api.download_image(self.token, message_id, image_key)

  async def download_file(
    self, message_id: str, file_key: str, file_name: str = "",
  ) -> str:
    return lark_api.download_file(self.token, message_id, file_key, file_name)

  async def add_reaction(self, message_id: str, emoji_type: str) -> str:
    return lark_api.add_reaction(self.token, message_id, emoji_type)

  async def remove_reaction(self, message_id: str, reaction_id: str) -> None:
    lark_api.remove_reaction(self.token, message_id, reaction_id)

  async def get_bot_id(self) -> str:
    bot_info = lark_api.get_bot_info(self.token)
    return bot_info.get("open_id", "")

  async def get_chat_members(self, chat_id: str) -> list[JsonObject]:
    return lark_api.get_chat_members(self.token, chat_id)

  async def lookup_open_id_by_email(self, email: str) -> str:
    return lark_api.lookup_open_id_by_email(self.token, email) or ""

  async def get_chat_info(self, chat_id: str) -> JsonObject:
    return lark_api.get_chat_info(self.token, chat_id)


  async def delete_message(self, message_id: str) -> None:
    lark_api.delete_message(self.token, message_id)

  async def resolve_operator_and_bot(self, email: str = "") -> tuple[str, str]:
    operator_open_id = ""
    if email:
      operator_open_id = lark_api.lookup_open_id_by_email(self.token, email) or ""
    bot_info = lark_api.get_bot_info(self.token)
    return operator_open_id, bot_info.get("open_id", "")

  async def ensure_workspace_claimed(self, project_dir: str, model: str) -> None:
    from .workspace import (ensure_workspace_tag, evict_existing,
                            claim_group, write_pid_file)
    ensure_workspace_tag(self.token, self.chat_id, project_dir)
    evict_existing(self.token, self.chat_id)
    claim_group(self.token, self.chat_id, model=model)
    write_pid_file(self.chat_id)

  async def update_workspace_tag(self, project_dir: str) -> None:
    from .workspace import ensure_workspace_tag
    ensure_workspace_tag(self.token, self.chat_id, project_dir)

  async def release_workspace(self) -> None:
    from .workspace import release_group, remove_pid_file
    release_group(self.token, self.chat_id)
    remove_pid_file(self.chat_id)

  async def update_status(self, model: str, state: str) -> None:
    from . import status_tab
    status_tab.update_status(self.token, self.chat_id, model, state)

  async def send_heartbeat(self, model: str) -> None:
    relay_url, _ = load_relay_config()
    if not relay_url:
      return
    from . import relay as relay_client
    from .workspace import get_machine_name
    relay_client.send_heartbeat(
      self.chat_id, pid=os.getpid(), model=model,
      machine=get_machine_name())

  async def dissolve_chat(self) -> None:
    lark_api.dissolve_chat(self.token, self.chat_id)

  @property
  def permission_active(self) -> bool:
    if self._events is None:
      return False
    return bool(self._events.permission_active)

  @permission_active.setter
  def permission_active(self, active: bool) -> None:
    if self._events is None:
      return
    self._events.permission_active = active
