"""cTrader price-augmented snapshot sender + Telegram Bot API forwarder.

Used by ForwarderCore when a forwarding pair has `price_augment` enabled or
`forward_via_bot` enabled.

- Price augmentation: builds a snapshot containing the live bid/ask at signal
  time, stores it in `signal_log`, and sends a formatted HTML message to the
  configured admin chat via the Telegram Bot API.
- Bot forwarding: sends a verbatim text message to a destination channel using
  the Bot API. This lets a bot (admin of the destination channel) own the
  forwarding surface, so downstream systems only need to talk to the bot via
  webhooks instead of dealing with the Telegram user-client side.
- Webhook: optionally POSTs the snapshot to a configured outbound URL so
  automation can react without touching Telegram at all.

Memory constraints (~250 MB shared-hosting budget) drove these choices:
- Uses urllib from the standard library for outgoing Bot API calls.
- CTraderSpotClient is minimal and avoids numpy / heavy execution layers.
- Keeps one persistent cTrader TCP+SSL connection so price is available
  immediately when a signal lands.
"""

from __future__ import annotations

import asyncio
import hashlib
import hmac
import json
import logging
import time
import urllib.error
import urllib.request
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Optional

from ctrader_spot_client import CTraderSpotClient, SpotTick
from signal_parser import SignalType

logger = logging.getLogger(__name__)

TELEGRAM_API = "https://api.telegram.org/bot{token}/{method}"


@dataclass(slots=True)
class ChannelPost:
    chat_id: int
    message_id: int
    text: str
    date: Optional[int] = None
    reply_to_message_id: Optional[int] = None
    sender_chat_id: Optional[int] = None


class SignalProcessor:
    """Build/send price-augmented snapshots and bot-forwarded messages."""

    def __init__(
        self,
        db,
        bot_token: str,
        admin_chat_id: Optional[int],
        symbol: str = "XAUUSD",
        price_timeout: float = 8.0,
        webhook_url: str = "",
        webhook_secret: str = "",
    ):
        self.db = db
        self.bot_token = bot_token
        self.admin_chat_id = admin_chat_id
        self.symbol = symbol.upper()
        self.price_timeout = price_timeout
        self.webhook_url = webhook_url.strip()
        self.webhook_secret = webhook_secret

    def update_config(
        self,
        bot_token: Optional[str] = None,
        admin_chat_id: Optional[int] = None,
        webhook_url: Optional[str] = None,
        webhook_secret: Optional[str] = None,
    ) -> None:
        if bot_token is not None:
            self.bot_token = bot_token
        if admin_chat_id is not None:
            self.admin_chat_id = admin_chat_id
        if webhook_url is not None:
            self.webhook_url = webhook_url.strip()
        if webhook_secret is not None:
            self.webhook_secret = webhook_secret

    # ── Price-augmented snapshot ─────────────────────────────────────────────

    async def send_augmented_snapshot(
        self,
        ctrader_client: CTraderSpotClient,
        source_chat_id: int,
        message_id: int,
        text: str,
        date: Optional[int] = None,
        reply_to_message_id: Optional[int] = None,
        symbol: Optional[str] = None,
    ) -> dict[str, Any]:
        """Fetch live price and send an augmented snapshot to the admin chat."""
        received_ms = int(time.time() * 1000)
        sym = (symbol or self.symbol).upper()

        tick = await ctrader_client.wait_for_tick(sym, timeout=self.price_timeout)
        fetched_ms = int(time.time() * 1000)

        post = ChannelPost(
            chat_id=source_chat_id,
            message_id=message_id,
            text=text,
            date=date,
            reply_to_message_id=reply_to_message_id,
        )
        snapshot = self._build_snapshot(post, tick, received_ms, fetched_ms, sym)
        await self._persist_snapshot(snapshot)

        if self.admin_chat_id:
            await self._send_augmented_message(post, snapshot)
        else:
            logger.warning("No admin chat configured; price snapshot not sent")

        return snapshot

    def _build_snapshot(
        self,
        post: ChannelPost,
        tick: Optional[SpotTick],
        received_ms: int,
        fetched_ms: int,
        symbol: str,
    ) -> dict[str, Any]:
        snapshot = {
            "source_chat_id": post.chat_id,
            "source_message_id": post.message_id,
            "signal_text": post.text,
            "signal_date": post.date,
            "reply_to_message_id": post.reply_to_message_id,
            "received_at_ms": received_ms,
            "price_fetched_at_ms": fetched_ms,
            "symbol": symbol,
            "price_available": tick is not None,
        }
        if tick:
            snapshot.update(
                {
                    "symbol_id": tick.symbol_id,
                    "bid": tick.bid,
                    "ask": tick.ask,
                    "spread": round(tick.ask - tick.bid, 5),
                    "tick_timestamp_ms": tick.timestamp_ms,
                }
            )
        return snapshot

    # ── Signal notification ──────────────────────────────────────────────────
    # Sent to admin chat for *every* non-promo signal (entry, manage, result,
    # close, etc.) so the full signal history is visible in Telegram.

    async def send_signal_notification(
        self,
        ctrader_client: CTraderSpotClient,
        source_chat_id: int,
        message_id: int,
        text: str,
        signal_type: str = "signal",
        date: Optional[int] = None,
        reply_to_message_id: Optional[int] = None,
        symbol: Optional[str] = None,
    ) -> None:
        """Send a simple notification for any signal type to the admin chat."""
        if not self.admin_chat_id:
            return
        if not self.bot_token:
            return

        sym = (symbol or self.symbol).upper()
        tick = ctrader_client.get_tick(sym) if ctrader_client else None
        price_line = ""
        if tick:
            price_line = (
                f"\n  Bid: <code>{tick.bid}</code>  Ask: <code>{tick.ask}</code>"
                f"  Spread: <code>{round(tick.ask - tick.bid, 5)}</code>"
            )

        safe_text = self._escape_html(text.strip() or "[no text]")
        if len(safe_text) > 400:
            body = f"<pre>{safe_text[:950]}</pre>"
        else:
            body = safe_text

        header = (
            f"<b>{signal_type.upper()}</b> "
            f"<code>#{message_id}</code>{price_line}"
        )

        lines = [header, "", body, ""]
        if reply_to_message_id:
            lines.append(f"<i>↳ reply to #{reply_to_message_id}</i>")

        msg_text = "\n".join(lines)
        try:
            await self._bot_post(
                "sendMessage",
                {
                    "chat_id": self.admin_chat_id,
                    "text": msg_text,
                    "parse_mode": "HTML",
                    "disable_web_page_preview": True,
                },
            )
        except Exception as exc:
            logger.exception("Signal notification failed: %s", exc)

    # ── Verbatim signal webhook ──────────────────────────────────────────────

    async def send_signal_webhook(
        self,
        source_chat_id: int,
        message_id: int,
        text: str,
        date: Optional[int] = None,
        reply_to_message_id: Optional[int] = None,
    ) -> None:
        """POST the raw, verbatim signal to SIGNAL_WEBHOOK_URL.

        This is called for *every* signal type (entry, update, close, TP hit,
        etc.) so the downstream pipeline receives the complete message history.
        """
        if not self.webhook_url:
            return

        received_ms = int(time.time() * 1000)
        payload = json.dumps(
            {
                "event": "signal",
                "received_at_ms": received_ms,
                "snapshot": {
                    "source_chat_id": source_chat_id,
                    "source_message_id": message_id,
                    "signal_text": text,
                    "signal_date": date,
                    "reply_to_message_id": reply_to_message_id,
                },
            },
            default=str,
        ).encode("utf-8")

        headers = {
            "Content-Type": "application/json",
            "User-Agent": "ssfx-signal-forwarder/1.0",
        }
        if self.webhook_secret:
            sig = hmac.new(
                self.webhook_secret.encode("utf-8"), payload, hashlib.sha256
            ).hexdigest()
            headers["X-Signal-Signature"] = f"sha256={sig}"

        def _request():
            req = urllib.request.Request(
                self.webhook_url, data=payload, headers=headers, method="POST"
            )
            with urllib.request.urlopen(req, timeout=15) as resp:
                return resp.status

        try:
            loop = asyncio.get_running_loop()
            status = await loop.run_in_executor(None, _request)
            logger.info("Signal webhook returned HTTP %s", status)
        except Exception as exc:
            logger.warning("Signal webhook failed: %s", exc)

    async def _persist_snapshot(self, snapshot: dict[str, Any]) -> None:
        """Offload blocking DB write to the executor used by Database."""
        loop = asyncio.get_running_loop()
        try:
            await loop.run_in_executor(None, self.db.save_signal_snapshot, snapshot)
        except Exception as exc:
            logger.exception("Failed to persist signal snapshot: %s", exc)

    # ── Bot forwarding (text only) ───────────────────────────────────────────

    async def send_bot_text(
        self,
        dest_chat_id: int,
        text: str,
        entities: Optional[list[dict]] = None,
        reply_to_message_id: Optional[int] = None,
        disable_link_preview: bool = True,
        bot_token: Optional[str] = None,
    ) -> Optional[int]:
        """Send a verbatim text message to a destination channel via the Bot API.

        If ``bot_token`` is provided it overrides the global default, enabling
        per-pair bot forwarding. Returns the sent message id on success, or None
        on failure.
        """
        token = bot_token or self.bot_token
        if not token:
            logger.error(
                "Bot token missing; cannot forward via bot to %s", dest_chat_id
            )
            return None

        payload: dict[str, Any] = {
            "chat_id": dest_chat_id,
            "text": text,
            "disable_web_page_preview": disable_link_preview,
        }
        if entities:
            payload["entities"] = entities
        if reply_to_message_id:
            payload["reply_to_message_id"] = reply_to_message_id

        try:
            result = await self._bot_post("sendMessage", payload, bot_token=token)
            msg = result.get("result", {})
            return msg.get("message_id")
        except Exception as exc:
            logger.exception("Bot sendMessage to %s failed: %s", dest_chat_id, exc)
            return None

    async def edit_bot_text(
        self,
        dest_chat_id: int,
        dest_message_id: int,
        text: str,
        entities: Optional[list[dict]] = None,
        bot_token: Optional[str] = None,
    ) -> bool:
        """Edit a message previously sent by the bot."""
        token = bot_token or self.bot_token
        if not token:
            return False
        payload: dict[str, Any] = {
            "chat_id": dest_chat_id,
            "message_id": dest_message_id,
            "text": text,
        }
        if entities:
            payload["entities"] = entities
        try:
            await self._bot_post("editMessageText", payload, bot_token=token)
            return True
        except Exception as exc:
            logger.exception(
                "Bot editMessageText %s/%s failed: %s",
                dest_chat_id,
                dest_message_id,
                exc,
            )
            return False

    async def delete_bot_message(
        self,
        dest_chat_id: int,
        dest_message_id: int,
        bot_token: Optional[str] = None,
    ) -> bool:
        """Delete a message previously sent by the bot."""
        token = bot_token or self.bot_token
        if not token:
            return False
        try:
            await self._bot_post(
                "deleteMessage",
                {"chat_id": dest_chat_id, "message_id": dest_message_id},
                bot_token=token,
            )
            return True
        except Exception as exc:
            logger.exception(
                "Bot deleteMessage %s/%s failed: %s",
                dest_chat_id,
                dest_message_id,
                exc,
            )
            return False

    # ── Telegram Bot API helpers ─────────────────────────────────────────────

    def _bot_url(self, method: str, bot_token: Optional[str] = None) -> str:
        return TELEGRAM_API.format(token=bot_token or self.bot_token, method=method)

    async def _send_augmented_message(
        self, post: ChannelPost, snapshot: dict[str, Any]
    ) -> None:
        if not self.admin_chat_id:
            return

        text = self._format_augmented_message(post, snapshot)
        try:
            await self._bot_post(
                "sendMessage",
                {
                    "chat_id": self.admin_chat_id,
                    "text": text,
                    "parse_mode": "HTML",
                    "disable_web_page_preview": True,
                },
            )
        except Exception as exc:
            logger.exception("Failed to send augmented message: %s", exc)

    def _format_augmented_message(
        self, post: ChannelPost, snapshot: dict[str, Any]
    ) -> str:
        header = f"<b>Price-augmented signal</b> (#{post.message_id})"
        lines = [header, ""]

        # Preserve original signal text inside a code block if it is long,
        # otherwise quote it plainly.
        signal_text = post.text.strip()
        if len(signal_text) > 400:
            lines.append(f"<pre>{self._escape_html(signal_text[:950])}</pre>")
        else:
            lines.append(self._escape_html(signal_text))
        lines.append("")

        if snapshot.get("price_available"):
            lines.extend(
                [
                    f"<b>{snapshot['symbol']}</b> at signal time:",
                    f"  Bid: <code>{snapshot['bid']}</code>",
                    f"  Ask: <code>{snapshot['ask']}</code>",
                    f"  Spread: <code>{snapshot['spread']}</code>",
                    f"  Latency: {snapshot['price_fetched_at_ms'] - snapshot['received_at_ms']} ms",
                    f"  cTrader tick ts: {self._format_ts(snapshot['tick_timestamp_ms'])} ({snapshot['tick_timestamp_ms']})",
                ]
            )
        else:
            lines.append("<i>No live cTrader tick was available.</i>")

        if post.reply_to_message_id:
            lines.append(f"  ↳ reply to msg #{post.reply_to_message_id}")

        return "\n".join(lines)

    @staticmethod
    def _escape_html(text: str) -> str:
        return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

    @staticmethod
    def _format_ts(ms: int) -> str:
        if not ms:
            return "n/a"
        dt = datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
        # e.g. "July 1, 2026 11:12:33 PM UTC" — no leading zero on day.
        return dt.strftime(f"%B {dt.day}, %Y %I:%M:%S %p UTC")

    async def _bot_post(
        self,
        method: str,
        payload: dict[str, Any],
        bot_token: Optional[str] = None,
    ) -> dict[str, Any]:
        """Call the Telegram Bot API using only standard-library urllib."""
        token = bot_token or self.bot_token
        url = self._bot_url(method, bot_token=token)
        data = json.dumps(payload).encode("utf-8")
        headers = {"Content-Type": "application/json"}

        def _request():
            req = urllib.request.Request(url, data=data, headers=headers, method="POST")
            try:
                with urllib.request.urlopen(req, timeout=30) as resp:
                    return json.loads(resp.read().decode("utf-8"))
            except urllib.error.HTTPError as exc:
                body = exc.read().decode("utf-8", errors="ignore")
                logger.error(
                    "Telegram Bot API %s failed: %s %s", method, exc.code, body
                )
                raise

        loop = asyncio.get_running_loop()
        return await loop.run_in_executor(None, _request)

    # ── Outbound webhook ─────────────────────────────────────────────────────

    async def _post_webhook(self, snapshot: dict[str, Any]) -> None:
        """POST the snapshot to SIGNAL_WEBHOOK_URL if configured."""
        if not self.webhook_url:
            return

        payload = json.dumps(
            {
                "event": "signal",
                "received_at_ms": snapshot.get("received_at_ms"),
                "snapshot": snapshot,
            },
            default=str,
        ).encode("utf-8")

        headers = {
            "Content-Type": "application/json",
            "User-Agent": "ssfx-signal-forwarder/1.0",
        }
        if self.webhook_secret:
            sig = hmac.new(
                self.webhook_secret.encode("utf-8"), payload, hashlib.sha256
            ).hexdigest()
            headers["X-Signal-Signature"] = f"sha256={sig}"

        def _request():
            req = urllib.request.Request(
                self.webhook_url, data=payload, headers=headers, method="POST"
            )
            with urllib.request.urlopen(req, timeout=15) as resp:
                return resp.status

        try:
            loop = asyncio.get_running_loop()
            status = await loop.run_in_executor(None, _request)
            logger.info("Signal webhook returned HTTP %s", status)
        except Exception as exc:
            logger.warning("Signal webhook failed: %s", exc)
