"""Telegram approval workflow — human-in-the-loop for outgoing comms + core KB changes.

Flow: Agent submits → Leo reviews substance → Bot sends to Cory → Cory approves/rejects.

Architecture:
- approval_queue table in pipeline.db (migration v11)
- Bot polls for leo_approved items, sends formatted Telegram messages with inline buttons
- Cory taps Approve/Reject → callback handler updates status
- 24h expiry timeout on all pending approvals

OPSEC: Content filter rejects submissions containing financial figures or deal-specific language.
No deal terms, no dollar amounts, no private investment details in approval requests — ever.

Epimetheus owns this module.
"""

from __future__ import annotations

# ruff: noqa: I001

import logging
import re
import sqlite3
from pathlib import Path

try:
    from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
    from telegram.ext import CallbackQueryHandler, ContextTypes
except ImportError:  # Optional in local unit tests that only exercise OPSEC logic.
    InlineKeyboardButton = None
    InlineKeyboardMarkup = None
    Update = None
    CallbackQueryHandler = None
    ContextTypes = None

logger = logging.getLogger("telegram.approvals")

# ─── OPSEC Content Filter ─────────────────────────────────────────────
# Reject submissions containing financial figures or deal-specific language.
# Pattern matches: $1M, $500K, 1.5 million, deal terms, valuation, cap table, etc.
OPSEC_PATTERNS = [
    re.compile(r"\$[\d,.]+[KMBkmb]?\b", re.IGNORECASE),          # $500K, $1.5M, $100
    re.compile(r"\b\d+[\d,.]*\s*(million|billion|thousand)\b", re.IGNORECASE),
    re.compile(r"\b(deal terms?|valuation|cap table|equity split|ownership stake|term sheet|dilution|fee split)\b", re.IGNORECASE),
    re.compile(r"\b(SAFE\s+(?:note|round|agreement)|SAFT|convertible note|preferred stock|liquidation preference)\b", re.IGNORECASE),
    re.compile(r"\bSeries\s+[A-Z]\b", re.IGNORECASE),            # Series A/B/C/F funding rounds
    re.compile(r"\b(partnership terms|committed to (?:the |a )?round|funding round|(?:pre-?)?seed round)\b", re.IGNORECASE),
]

# Sensitive entity names — loaded from opsec-entities.txt config file.
# Edit the config file to add/remove entities without code changes.
_OPSEC_ENTITIES_FILE = Path(__file__).parent / "opsec-entities.txt"


def _load_sensitive_entities() -> list[re.Pattern]:
    """Load sensitive entity patterns from config file."""
    patterns = []
    if _OPSEC_ENTITIES_FILE.exists():
        for line in _OPSEC_ENTITIES_FILE.read_text().splitlines():
            line = line.strip()
            if line and not line.startswith("#"):
                patterns.append(re.compile(rf"\b{line}\b", re.IGNORECASE))
    return patterns


SENSITIVE_ENTITIES = _load_sensitive_entities()


def check_opsec(content: str) -> str | None:
    """Check content against OPSEC patterns. Returns violation description or None."""
    for pattern in OPSEC_PATTERNS:
        match = pattern.search(content)
        if match:
            return f"OPSEC violation: content contains '{match.group()}' — no financial figures or deal terms in approval requests"
    for pattern in SENSITIVE_ENTITIES:
        match = pattern.search(content)
        if match:
            return f"OPSEC violation: content references sensitive entity '{match.group()}' — deal-adjacent entities blocked"
    return None


# ─── Message Formatting ───────────────────────────────────────────────

TYPE_LABELS = {
    "tweet": "Tweet",
    "kb_change": "KB Change",
    "architecture_change": "Architecture Change",
    "public_post": "Public Post",
    "position": "Position",
    "agent_structure": "Agent Structure",
}

# ─── Tier Classification ─────────────────────────────────────────────
# Tier 1: Must approve (outgoing, public, irreversible)
# Tier 2: Should approve (core architecture, strategic)
# Tier 3: Autonomous (no approval needed — goes to daily digest only)

TIER_1_TYPES = {"tweet", "public_post", "position"}
TIER_2_TYPES = {"kb_change", "architecture_change", "agent_structure"}
# Everything else is Tier 3 — no approval queue entry, digest only


def classify_tier(approval_type: str) -> int:
    """Classify an approval request into tier 1, 2, or 3."""
    if approval_type in TIER_1_TYPES:
        return 1
    if approval_type in TIER_2_TYPES:
        return 2
    return 3


def format_approval_message(row: sqlite3.Row) -> str:
    """Format an approval request for Telegram display."""
    type_label = TYPE_LABELS.get(row["type"], row["type"].replace("_", " ").title())
    agent = row["originating_agent"].title()
    content = row["content"]

    # Truncate long content for Telegram (4096 char limit)
    if len(content) > 3000:
        content = content[:3000] + "\n\n[... truncated]"

    parts = [
        "APPROVAL REQUEST",
        "",
        f"Type: {type_label}",
        f"From: {agent}",
    ]

    if row["context"]:
        parts.append(f"Context: {row['context']}")

    if row["leo_review_note"]:
        parts.append(f"Leo review: {row['leo_review_note']}")

    parts.extend([
        "",
        "---",
        content,
        "---",
    ])

    return "\n".join(parts)


def build_keyboard(request_id: int) -> InlineKeyboardMarkup:
    """Build inline keyboard with Approve/Reject buttons."""
    if InlineKeyboardMarkup is None or InlineKeyboardButton is None:
        raise ImportError("python-telegram-bot is required to build approval keyboards")
    return InlineKeyboardMarkup([
        [
            InlineKeyboardButton("Approve", callback_data=f"approve:{request_id}"),
            InlineKeyboardButton("Reject", callback_data=f"reject:{request_id}"),
        ]
    ])


# ─── Core Logic ───────────────────────────────────────────────────────

def get_pending_for_cory(conn: sqlite3.Connection) -> list[sqlite3.Row]:
    """Get approval requests that Leo approved and are ready for Cory."""
    return conn.execute(
        """SELECT * FROM approval_queue
           WHERE leo_review_status = 'leo_approved'
             AND status = 'pending'
             AND telegram_message_id IS NULL
             AND (expires_at IS NULL OR expires_at > datetime('now'))
           ORDER BY submitted_at ASC""",
    ).fetchall()


def expire_stale_requests(conn: sqlite3.Connection) -> int:
    """Expire requests older than 24h. Returns count expired."""
    cursor = conn.execute(
        """UPDATE approval_queue
           SET status = 'expired', decided_at = datetime('now')
           WHERE status = 'pending'
             AND expires_at IS NOT NULL
             AND expires_at <= datetime('now')""",
    )
    if cursor.rowcount > 0:
        conn.commit()
        logger.info("Expired %d stale approval requests", cursor.rowcount)
    return cursor.rowcount


def record_decision(
    conn: sqlite3.Connection,
    request_id: int,
    decision: str,
    decision_by: str,
    rejection_reason: str = None,
) -> bool:
    """Record an approval/rejection decision. Returns True if updated."""
    cursor = conn.execute(
        """UPDATE approval_queue
           SET status = ?, decision_by = ?, rejection_reason = ?,
               decided_at = datetime('now')
           WHERE id = ? AND status = 'pending'""",
        (decision, decision_by, rejection_reason, request_id),
    )
    conn.commit()
    return cursor.rowcount > 0


def record_telegram_message(conn: sqlite3.Connection, request_id: int, message_id: int):
    """Record the Telegram message ID for an approval notification."""
    conn.execute(
        "UPDATE approval_queue SET telegram_message_id = ? WHERE id = ?",
        (message_id, request_id),
    )
    conn.commit()


# ─── Telegram Handlers ────────────────────────────────────────────────

async def handle_approval_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle Approve/Reject button taps from Cory."""
    query = update.callback_query
    await query.answer()

    data = query.data
    if not data or ":" not in data:
        return

    action, request_id_str = data.split(":", 1)
    if action not in ("approve", "reject"):
        return

    try:
        request_id = int(request_id_str)
    except ValueError:
        return

    conn = context.bot_data.get("approval_conn")
    if not conn:
        await query.edit_message_text("Error: approval DB not connected")
        return

    if action == "reject":
        # For rejection, edit the message to ask for reason
        row = conn.execute(
            "SELECT * FROM approval_queue WHERE id = ?", (request_id,)
        ).fetchone()
        if not row or row["status"] != "pending":
            await query.edit_message_text("This request has already been processed.")
            return

        # Store pending rejection — user can reply with reason
        context.bot_data[f"pending_reject:{request_id}"] = True
        await query.edit_message_text(
            f"{query.message.text}\n\nRejected. Reply to this message with feedback for the agent (optional).",
        )
        record_decision(conn, request_id, "rejected", query.from_user.username or str(query.from_user.id))
        logger.info("Approval #%d REJECTED by %s", request_id, query.from_user.username)
        return

    # Approve
    user = query.from_user.username or str(query.from_user.id)
    success = record_decision(conn, request_id, "approved", user)

    if success:
        # Check if this is a tweet — if so, auto-post to X
        row = conn.execute(
            "SELECT type FROM approval_queue WHERE id = ?", (request_id,)
        ).fetchone()

        post_status = ""
        if row and row["type"] == "tweet":
            try:
                from x_publisher import handle_approved_tweet
                result = await handle_approved_tweet(conn, request_id)
                if result.get("success"):
                    url = result.get("tweet_url", "")
                    post_status = f"\n\nPosted to X: {url}"
                    logger.info("Tweet #%d auto-posted: %s", request_id, url)
                else:
                    error = result.get("error", "unknown error")
                    post_status = f"\n\nPost failed: {error}"
                    logger.error("Tweet #%d auto-post failed: %s", request_id, error)
            except Exception as e:
                post_status = f"\n\nPost failed: {e}"
                logger.error("Tweet #%d auto-post error: %s", request_id, e)

        await query.edit_message_text(
            f"{query.message.text}\n\nAPPROVED by {user}{post_status}"
        )
        logger.info("Approval #%d APPROVED by %s", request_id, user)
    else:
        await query.edit_message_text("This request has already been processed.")


async def handle_rejection_reply(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Capture rejection reason from reply to a rejected approval message."""
    if not update.message or not update.message.reply_to_message:
        return False

    # Check if the replied-to message is a rejected approval
    conn = context.bot_data.get("approval_conn")
    if not conn:
        return False

    reply_msg_id = update.message.reply_to_message.message_id
    row = conn.execute(
        "SELECT id FROM approval_queue WHERE telegram_message_id = ? AND status = 'rejected'",
        (reply_msg_id,),
    ).fetchone()

    if not row:
        return False

    # Update rejection reason
    reason = update.message.text.strip()
    conn.execute(
        "UPDATE approval_queue SET rejection_reason = ? WHERE id = ?",
        (reason, row["id"]),
    )
    conn.commit()
    await update.message.reply_text(f"Feedback recorded for approval #{row['id']}.")
    logger.info("Rejection reason added for approval #%d: %s", row["id"], reason[:100])
    return True


# ─── Poll Job ─────────────────────────────────────────────────────────

async def poll_approvals(context: ContextTypes.DEFAULT_TYPE):
    """Poll for Leo-approved requests and send to Cory. Runs every 30s."""
    conn = context.bot_data.get("approval_conn")
    admin_chat_id = context.bot_data.get("admin_chat_id")

    if not conn or not admin_chat_id:
        return

    # Expire stale requests first (may fail on DB lock - retry next cycle)
    try:
        expire_stale_requests(conn)
    except Exception:
        pass  # non-fatal, retries in 30s

    # Send new notifications
    pending = get_pending_for_cory(conn)
    for row in pending:
        try:
            text = format_approval_message(row)
            keyboard = build_keyboard(row["id"])
            msg = await context.bot.send_message(
                chat_id=admin_chat_id,
                text=text,
                reply_markup=keyboard,
            )
            record_telegram_message(conn, row["id"], msg.message_id)
            logger.info("Sent approval #%d to admin (type=%s, agent=%s)",
                       row["id"], row["type"], row["originating_agent"])
        except Exception as e:
            logger.error("Failed to send approval #%d: %s", row["id"], e)
