"""
Channel Hub — AwoooP 入站事件統一路由 + Progressive Feedback Policy
====================================================================
AwoooP Phase 7: ADR-106（channel_event family）
2026-05-04 ogt + Claude Sonnet 4.6

功能：
  1. Telegram 入站事件鏡像（記錄到 awooop_conversation_event）
  2. 建立 platform run（呼叫 platform_runtime.create_run）
  3. Progressive Feedback Policy：
     - run 進入 WAITING_TOOL 狀態 → 30 秒後若未 complete → 發 interim Telegram 訊息
     - 訊息記錄到 awooop_outbound_message
  4. Shadow Mode：不發任何 Telegram 訊息（只記錄到 outbound_message, status='shadow'）

Progressive Feedback Policy 設計（ADR-106 P2-03）：
  - 用 asyncio.create_task 啟動 30s 計時器
  - 30s 後查詢 run state：若仍在 WAITING_TOOL → 發 interim 訊息
  - interim 訊息：「AI 正在分析中，請稍候...」（不洩漏 run 細節）
  - Final reply 由 shadow_execute() 完成後觸發（Phase 8 實作）

與 legacy telegram_gateway.py 的關係：
  - 完全獨立，不修改 legacy gateway
  - legacy 繼續處理 legacy flow（signal_worker 觸發的 approval/notification）
  - AwoooP run 只走本模組
"""

from __future__ import annotations

import asyncio
import hashlib
import html
import json
import re
from datetime import UTC, datetime
from typing import Any
from uuid import NAMESPACE_URL, UUID, uuid5

import structlog
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession

from src.db.awooop_models import AwoooPRunState
from src.services.audit_sink import _redact_string, sanitize
from src.services.platform_runtime import create_run

logger = structlog.get_logger(__name__)

# Progressive Feedback Policy：等待超過此秒數才發 interim 訊息
_INTERIM_WAIT_SECONDS = 30
_INBOUND_REDACTION_VERSION = "audit_sink_v1"
_OUTBOUND_REDACTION_VERSION = "audit_sink_v1"
_INCIDENT_ID_RE = re.compile(r"\bINC-\d{8}-[A-Z0-9]{6}\b")


def _db_timestamp_now() -> datetime:
    """Return UTC now in the timestamp shape accepted by the production DB path."""
    return datetime.now(UTC).replace(tzinfo=None)


def _compact_unique(values: list[str | None], *, limit: int = 20) -> list[str]:
    """Return stable non-empty values without leaking duplicate source refs."""
    return sorted({str(value).strip() for value in values if str(value or "").strip()})[:limit]


def build_inbound_source_envelope(
    *,
    provider: str,
    stage: str,
    provider_event_id: str,
    raw_event_id: str | None = None,
    raw_content: str | None = None,
    alertname: str | None = None,
    severity: str | None = None,
    namespace: str | None = None,
    target_resource: str | None = None,
    fingerprint: str | None = None,
    incident_id: str | None = None,
    approval_id: str | None = None,
    source_url: str | None = None,
    labels: dict[str, Any] | None = None,
    annotations: dict[str, Any] | None = None,
    extra: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Build a redaction-friendly inbound replay envelope for truth-chain use."""
    content_sha256 = hashlib.sha256(raw_content.encode()).hexdigest() if raw_content else None
    text_refs = _INCIDENT_ID_RE.findall(raw_content or "")
    provider_name = str(provider or "unknown").strip().lower() or "unknown"
    is_provider_heartbeat = str(stage or "").strip().lower() == "heartbeat"
    source_refs = {
        "event_ids": _compact_unique([raw_event_id]),
        "incident_ids": _compact_unique([incident_id, *text_refs]),
        "approval_ids": _compact_unique([approval_id]),
        "alert_ids": _compact_unique([provider_event_id, raw_event_id]),
        "fingerprints": _compact_unique([fingerprint]),
        "sentry_issue_ids": _compact_unique(
            [raw_event_id, provider_event_id]
            if provider_name == "sentry" and not is_provider_heartbeat
            else []
        ),
        "signoz_alerts": _compact_unique(
            [raw_event_id, alertname]
            if provider_name == "signoz" and not is_provider_heartbeat
            else []
        ),
    }
    envelope: dict[str, Any] = {
        "schema_version": "inbound_source_envelope_v1",
        "redaction_version": _INBOUND_REDACTION_VERSION,
        "adapter": f"{provider_name}_webhook",
        "provider": provider_name,
        "stage": stage,
        "provider_event_id": provider_event_id,
        "source_url": source_url,
        "content_sha256": content_sha256,
        "content_length": len(raw_content) if raw_content is not None else 0,
        "source_refs": source_refs,
        "log_correlation": {
            "alertname": alertname,
            "severity": severity,
            "namespace": namespace,
            "target_resource": target_resource,
            "fingerprint": fingerprint,
        },
        "labels": labels or {},
        "annotations": annotations or {},
    }
    if extra:
        envelope["extra"] = extra
    sanitized = sanitize(envelope)
    sanitized["content_sha256"] = content_sha256
    return sanitized


def _input_sha256(input_payload: dict[str, Any] | None) -> str | None:
    """計算 Run input 的穩定 hash，讓 mirror run 也能保留最小完整性證據。"""
    if not input_payload:
        return None
    canonical = json.dumps(
        input_payload,
        sort_keys=True,
        separators=(",", ":"),
        ensure_ascii=False,
    )
    return hashlib.sha256(canonical.encode()).hexdigest()


async def ensure_completed_shadow_run(
    db: AsyncSession,
    *,
    project_id: str,
    run_id: UUID,
    agent_id: str,
    trigger_type: str,
    trigger_ref: str | None,
    input_payload: dict[str, Any] | None = None,
) -> bool:
    """為 legacy mirror 資料補一筆 completed shadow run。

    AwoooP 在 strangler 階段會先 mirror legacy Telegram / alert-grouping
    資料。這些事件不應重新觸發 runtime，但需要 run_state 當 Console 的
    聚合錨點；因此這裡建立的是已完成的 shadow run，不會被 worker pick up。
    """
    result = await db.execute(
        text("""
            INSERT INTO awooop_run_state (
                run_id, project_id, agent_id, state,
                trigger_type, trigger_ref, is_shadow,
                input_sha256,
                attempt_count, max_attempts, cost_usd, step_count,
                created_at, completed_at, timeout_at
            ) VALUES (
                :run_id, :project_id, :agent_id, 'completed',
                :trigger_type, :trigger_ref, TRUE,
                :input_sha256,
                0, 3, 0.0000, 0,
                NOW(), NOW(), NOW()
            )
            ON CONFLICT (run_id) DO NOTHING
            RETURNING run_id
        """),
        {
            "run_id": run_id,
            "project_id": project_id,
            "agent_id": agent_id,
            "trigger_type": trigger_type,
            "trigger_ref": trigger_ref,
            "input_sha256": _input_sha256(input_payload),
        },
    )
    inserted = result.fetchone() is not None
    if inserted:
        logger.info(
            "completed_shadow_run_created",
            project_id=project_id,
            run_id=str(run_id),
            agent_id=agent_id,
            trigger_type=trigger_type,
        )
    return inserted


def build_grouped_alert_run_id(project_id: str, provider_event_id: str) -> UUID:
    """為 grouped child alert 建立穩定 run_id，讓 Run Monitor 可回查。"""
    return uuid5(NAMESPACE_URL, f"awooop:grouped-alert:{project_id}:{provider_event_id}")


def build_alertmanager_provider_event_id(alert_id: str, fingerprint: str, stage: str) -> str:
    """建立 Alertmanager inbound event 的冪等 provider_event_id。"""
    safe_alert_id = str(alert_id).strip() or "unknown"
    safe_fingerprint = str(fingerprint).strip()[:32] or "no-fingerprint"
    safe_stage = str(stage).strip()[:32] or "received"
    return f"alertmanager:{safe_stage}:{safe_alert_id}:{safe_fingerprint}"


def build_alertmanager_run_id(project_id: str, provider_event_id: str) -> UUID:
    """為 Alertmanager inbound mirror 建立穩定 shadow run_id。"""
    return uuid5(NAMESPACE_URL, f"awooop:alertmanager:{project_id}:{provider_event_id}")


def build_external_alert_provider_event_id(provider: str, event_id: str, stage: str) -> str:
    """建立 Sentry/SignOz 等外部告警 inbound event 的冪等 provider_event_id。"""
    safe_provider = str(provider).strip().lower()[:32] or "external"
    safe_event_id = str(event_id).strip()[:96] or "unknown"
    safe_stage = str(stage).strip()[:32] or "received"
    return f"{safe_provider}:{safe_stage}:{safe_event_id}"


def build_external_alert_run_id(project_id: str, provider_event_id: str) -> UUID:
    """為外部告警 inbound mirror 建立穩定 shadow run_id。"""
    return uuid5(NAMESPACE_URL, f"awooop:external-alert:{project_id}:{provider_event_id}")


# ─────────────────────────────────────────────────────────────────────────────
# 入站事件記錄
# ─────────────────────────────────────────────────────────────────────────────

async def mirror_inbound_event(
    db: AsyncSession,
    *,
    project_id: str,
    channel_type: str,
    provider_event_id: str,
    platform_subject_id: str | None = None,
    channel_user_id: str | None = None,
    channel_chat_id: str | None = None,
    content_type: str = "text",
    raw_content: str | None = None,
    source_envelope: dict[str, Any] | None = None,
    attachment_sha256: str | None = None,
    provider_ts: datetime | None = None,
    run_id: UUID | None = None,
    is_duplicate: bool = False,
) -> UUID:
    """
    記錄入站 channel event 到 awooop_conversation_event。

    raw_content 只用於計算 hash 和 preview，不入庫明文。
    回傳 event_id。
    """
    content_hash: str | None = None
    content_preview: str | None = None
    content_redacted: str | None = None

    if raw_content is not None:
        content_hash = hashlib.sha256(raw_content.encode()).hexdigest()
        # preview：redact 後截取前 256 字元
        content_redacted = _redact_string(raw_content)
        content_preview = (
            content_redacted[:256] if len(content_redacted) > 256 else content_redacted
        )

    if source_envelope and source_envelope.get("schema_version") == "inbound_source_envelope_v1":
        original_content_sha256 = source_envelope.get("content_sha256")
        envelope = sanitize(source_envelope)
        envelope.setdefault("redaction_version", _INBOUND_REDACTION_VERSION)
        envelope["content_sha256"] = content_hash or original_content_sha256
        envelope.setdefault("content_length", len(raw_content) if raw_content is not None else 0)
    else:
        envelope = build_inbound_source_envelope(
            provider=channel_type,
            stage="received",
            provider_event_id=provider_event_id,
            raw_event_id=provider_event_id,
            raw_content=raw_content,
            extra=source_envelope,
        )
    source_envelope_json = json.dumps(envelope, ensure_ascii=False, default=str)

    result = await db.execute(
        text("""
            INSERT INTO awooop_conversation_event (
                project_id, channel_type, provider_event_id,
                platform_subject_id, channel_user_id, channel_chat_id,
                run_id, content_type, content_hash, content_preview,
                content_redacted, redaction_version, source_envelope,
                attachment_sha256, is_duplicate, provider_ts, received_at
            ) VALUES (
                :project_id, :channel_type, :provider_event_id,
                :platform_subject_id, :channel_user_id, :channel_chat_id,
                :run_id, :content_type, :content_hash, :content_preview,
                :content_redacted, :redaction_version, CAST(:source_envelope AS jsonb),
                :attachment_sha256, :is_duplicate, :provider_ts, NOW()
            )
            ON CONFLICT (project_id, channel_type, provider_event_id) DO UPDATE SET
                is_duplicate = TRUE,
                run_id = COALESCE(EXCLUDED.run_id, awooop_conversation_event.run_id),
                content_redacted = COALESCE(
                    awooop_conversation_event.content_redacted,
                    EXCLUDED.content_redacted
                ),
                redaction_version = EXCLUDED.redaction_version,
                source_envelope = CASE
                    WHEN awooop_conversation_event.source_envelope = '{}'::jsonb
                    THEN EXCLUDED.source_envelope
                    ELSE awooop_conversation_event.source_envelope
                END
            RETURNING event_id
        """),
        {
            "project_id": project_id,
            "channel_type": channel_type,
            "provider_event_id": provider_event_id,
            "platform_subject_id": platform_subject_id,
            "channel_user_id": channel_user_id,
            "channel_chat_id": channel_chat_id,
            "run_id": run_id,
            "content_type": content_type,
            "content_hash": content_hash,
            "content_preview": content_preview,
            "content_redacted": content_redacted,
            "redaction_version": _INBOUND_REDACTION_VERSION,
            "source_envelope": source_envelope_json,
            "attachment_sha256": attachment_sha256,
            "is_duplicate": is_duplicate,
            "provider_ts": provider_ts,
        },
    )
    row = result.fetchone()
    event_id: UUID = row[0]
    logger.info(
        "channel_event_mirrored",
        project_id=project_id,
        channel_type=channel_type,
        event_id=str(event_id),
        is_duplicate=is_duplicate,
    )
    return event_id


def build_grouped_alert_provider_event_id(alert_id: str, fingerprint: str) -> str:
    """建立 grouped child alert 的冪等 provider_event_id。"""
    safe_alert_id = str(alert_id).strip() or "unknown"
    safe_fingerprint = str(fingerprint).strip()[:32] or "no-fingerprint"
    return f"alert-group:{safe_alert_id}:{safe_fingerprint}"


def format_alertmanager_event_content(
    *,
    stage: str,
    alert_id: str,
    alertname: str,
    severity: str,
    namespace: str,
    target_resource: str,
    fingerprint: str,
    notification_type: str | None = None,
    alert_category: str | None = None,
    incident_id: str | None = None,
    approval_id: str | None = None,
    repeat_count: int | None = None,
) -> str:
    """格式化 Alertmanager inbound mirror 摘要，讓 truth-chain 可回查。"""
    head = f"Incident: {incident_id}" if incident_id else f"Fingerprint: {fingerprint}"
    return "\n".join(
        [
            f"Alertmanager inbound {stage}",
            head,
            f"Alert ID: {alert_id}",
            f"Approval: {approval_id or '-'}",
            f"Alert: {alertname}",
            f"Severity: {severity}",
            f"Namespace: {namespace or 'default'}",
            f"Target: {target_resource or '-'}",
            f"Fingerprint: {fingerprint}",
            f"Notification Type: {notification_type or '-'}",
            f"Alert Category: {alert_category or '-'}",
            f"Repeat Count: {repeat_count if repeat_count is not None else '-'}",
        ]
    )


def format_grouped_alert_event_content(
    *,
    alert_id: str,
    alertname: str,
    severity: str,
    namespace: str,
    target_resource: str,
    group_key: str,
    count: int,
    parent_fingerprint: str | None,
    fingerprint: str,
) -> str:
    """格式化只落 AwoooP、不發 Telegram 的告警收斂事件摘要。"""
    parent = parent_fingerprint or "-"
    target = target_resource or "-"
    ns = namespace or "default"
    return "\n".join(
        [
            "告警已收斂，不發 Telegram",
            f"Alert ID: {alert_id}",
            f"Alert: {alertname}",
            f"Severity: {severity}",
            f"Namespace: {ns}",
            f"Target: {target}",
            f"Group: {group_key}",
            f"Group Count: {count}",
            f"Parent Fingerprint: {parent}",
            f"Child Fingerprint: {fingerprint}",
        ]
    )


def format_grouped_alert_digest_text(
    *,
    alertname: str,
    severity: str,
    namespace: str,
    target_resource: str,
    group_key: str,
    count: int,
) -> str:
    """格式化要回覆到父告警卡的短 digest。"""
    safe_alert = html.escape(alertname or "unknown")
    safe_severity = html.escape(severity or "unknown")
    safe_namespace = html.escape(namespace or "default")
    safe_target = html.escape(target_resource or "unknown")
    safe_group = html.escape(group_key or "unknown")

    return "\n".join(
        [
            "🧩 <b>告警已收斂到父卡</b>",
            f"├ 類型：<code>{safe_alert}</code>",
            f"├ 等級：<code>{safe_severity}</code>",
            f"├ 範圍：<code>{safe_namespace}</code>",
            f"├ 最新目標：<code>{safe_target}</code>",
            f"├ 群組：<code>{safe_group}</code>",
            f"└ 目前視窗：<b>{count}</b> 筆同組告警",
            "",
            "完整子告警請看 AwoooP Run 監控，不再逐筆發 Telegram。",
        ]
    )


async def maybe_send_grouped_alert_digest(
    *,
    project_id: str,
    alertname: str,
    severity: str,
    namespace: str,
    target_resource: str,
    group_key: str,
    count: int,
    parent_fingerprint: str | None,
) -> bool:
    """若父告警卡已存在，回覆一則低頻 digest；找不到父卡則安靜降級。"""
    if not parent_fingerprint:
        return False

    try:
        from sqlalchemy import select

        from src.db.base import get_db_context
        from src.db.models import ApprovalRecord
        from src.services.telegram_gateway import get_telegram_gateway

        async with get_db_context(project_id) as db:
            result = await db.execute(
                select(ApprovalRecord.incident_id)
                .where(ApprovalRecord.fingerprint == parent_fingerprint)
                .where(ApprovalRecord.incident_id.is_not(None))
                .order_by(ApprovalRecord.created_at.desc())
                .limit(1)
            )
            incident_id = result.scalar_one_or_none()

        if not incident_id:
            logger.info(
                "grouped_alert_digest_parent_not_ready",
                project_id=project_id,
                group_key=group_key,
                parent_fingerprint=parent_fingerprint,
            )
            return False

        digest_text = format_grouped_alert_digest_text(
            alertname=alertname,
            severity=severity,
            namespace=namespace,
            target_resource=target_resource,
            group_key=group_key,
            count=count,
        )
        sent = await get_telegram_gateway().append_grouped_alert_digest(
            incident_id=str(incident_id),
            group_key=group_key,
            digest_text=digest_text,
        )
        logger.info(
            "grouped_alert_digest_result",
            project_id=project_id,
            incident_id=str(incident_id),
            group_key=group_key,
            count=count,
            sent=sent,
        )
        return sent
    except Exception as exc:
        logger.warning(
            "grouped_alert_digest_failed",
            project_id=project_id,
            group_key=group_key,
            parent_fingerprint=parent_fingerprint,
            error=str(exc),
        )
        return False


async def record_grouped_alert_event(
    *,
    project_id: str,
    alert_id: str,
    alertname: str,
    severity: str,
    namespace: str,
    target_resource: str,
    group_key: str,
    count: int,
    parent_fingerprint: str | None,
    fingerprint: str,
) -> UUID | None:
    """
    將被 AlertGroupingService 收斂的子告警落到 AwoooP conversation_event。

    這條路徑刻意不發 Telegram，只保留 operator-facing 脈絡：
    - 群組不洗版
    - Console 仍能看到同組告警正在持續發生
    - DB 失敗 fail-open，不影響 Alertmanager webhook ACK
    """
    try:
        from src.db.base import get_db_context

        provider_event_id = build_grouped_alert_provider_event_id(alert_id, fingerprint)
        content = format_grouped_alert_event_content(
            alert_id=alert_id,
            alertname=alertname,
            severity=severity,
            namespace=namespace,
            target_resource=target_resource,
            group_key=group_key,
            count=count,
            parent_fingerprint=parent_fingerprint,
            fingerprint=fingerprint,
        )

        async with get_db_context(project_id) as db:
            run_id = build_grouped_alert_run_id(project_id, provider_event_id)
            await ensure_completed_shadow_run(
                db,
                project_id=project_id,
                run_id=run_id,
                agent_id="legacy-alert-grouping",
                trigger_type="grouped_alert_event",
                trigger_ref=provider_event_id,
                input_payload={
                    "alert_id": alert_id,
                    "alertname": alertname,
                    "severity": severity,
                    "group_key": group_key,
                    "fingerprint": fingerprint,
                },
            )
            event_id = await mirror_inbound_event(
                db,
                project_id=project_id,
                channel_type="internal",
                provider_event_id=provider_event_id,
                platform_subject_id="alertmanager",
                channel_user_id="alertmanager",
                channel_chat_id=f"alert-group:{group_key}",
                content_type="text",
                raw_content=content,
                provider_ts=_db_timestamp_now(),
                run_id=run_id,
            )

        logger.info(
            "grouped_alert_event_recorded",
            project_id=project_id,
            alert_id=alert_id,
            event_id=str(event_id),
            group_key=group_key,
            count=count,
        )
        await maybe_send_grouped_alert_digest(
            project_id=project_id,
            alertname=alertname,
            severity=severity,
            namespace=namespace,
            target_resource=target_resource,
            group_key=group_key,
            count=count,
            parent_fingerprint=parent_fingerprint,
        )
        return event_id
    except Exception as exc:
        logger.warning(
            "grouped_alert_event_record_failed",
            project_id=project_id,
            alert_id=alert_id,
            group_key=group_key,
            error=str(exc),
        )
        return None


async def record_alertmanager_event(
    *,
    project_id: str,
    alert_id: str,
    alertname: str,
    severity: str,
    namespace: str,
    target_resource: str,
    fingerprint: str,
    stage: str,
    notification_type: str | None = None,
    alert_category: str | None = None,
    incident_id: str | None = None,
    approval_id: str | None = None,
    repeat_count: int | None = None,
    is_duplicate: bool = False,
    source_url: str | None = None,
    labels: dict[str, Any] | None = None,
    annotations: dict[str, Any] | None = None,
    source_extra: dict[str, Any] | None = None,
) -> UUID | None:
    """
    將 Alertmanager inbound alert 鏡像到 AwoooP conversation_event。

    Telegram 不應是唯一事實來源；每個 firing alert 至少要有 received
    event，建立 incident/approval 後再補 incident_linked event 供 truth-chain
    依 incident_id 回查。DB 失敗 fail-open，不影響 Alertmanager ACK。
    """
    try:
        from src.db.base import get_db_context

        incident_ref = str(incident_id) if incident_id else None
        approval_ref = str(approval_id) if approval_id else None
        provider_event_id = build_alertmanager_provider_event_id(
            alert_id=alert_id,
            fingerprint=fingerprint,
            stage=stage,
        )
        content = format_alertmanager_event_content(
            stage=stage,
            alert_id=alert_id,
            alertname=alertname,
            severity=severity,
            namespace=namespace,
            target_resource=target_resource,
            fingerprint=fingerprint,
            notification_type=notification_type,
            alert_category=alert_category,
            incident_id=incident_ref,
            approval_id=approval_ref,
            repeat_count=repeat_count,
        )
        source_envelope = build_inbound_source_envelope(
            provider="alertmanager",
            stage=stage,
            provider_event_id=provider_event_id,
            raw_event_id=alert_id,
            raw_content=content,
            alertname=alertname,
            severity=severity,
            namespace=namespace,
            target_resource=target_resource,
            fingerprint=fingerprint,
            incident_id=incident_ref,
            approval_id=approval_ref,
            source_url=source_url,
            labels=labels,
            annotations=annotations,
            extra={
                "notification_type": notification_type,
                "alert_category": alert_category,
                "repeat_count": repeat_count,
                **(source_extra or {}),
            },
        )

        async with get_db_context(project_id) as db:
            run_id = build_alertmanager_run_id(project_id, provider_event_id)
            await ensure_completed_shadow_run(
                db,
                project_id=project_id,
                run_id=run_id,
                agent_id="legacy-alertmanager-webhook",
                trigger_type="alertmanager_inbound",
                trigger_ref=provider_event_id,
                input_payload={
                    "stage": stage,
                    "alert_id": alert_id,
                    "alertname": alertname,
                    "severity": severity,
                    "namespace": namespace,
                    "target_resource": target_resource,
                    "fingerprint": fingerprint,
                    "notification_type": notification_type,
                    "alert_category": alert_category,
                    "incident_id": incident_ref,
                    "approval_id": approval_ref,
                    "repeat_count": repeat_count,
                },
            )
            event_id = await mirror_inbound_event(
                db,
                project_id=project_id,
                channel_type="internal",
                provider_event_id=provider_event_id,
                platform_subject_id="alertmanager",
                channel_user_id="alertmanager",
                channel_chat_id=f"alertmanager:{namespace or 'default'}",
                content_type="text",
                raw_content=content,
                source_envelope=source_envelope,
                provider_ts=_db_timestamp_now(),
                run_id=run_id,
                is_duplicate=is_duplicate,
            )

        logger.info(
            "alertmanager_event_recorded",
            project_id=project_id,
            alert_id=alert_id,
            event_id=str(event_id),
            stage=stage,
            incident_id=incident_ref,
            fingerprint=fingerprint,
        )
        return event_id
    except Exception as exc:
        logger.warning(
            "alertmanager_event_record_failed",
            project_id=project_id,
            alert_id=alert_id,
            stage=stage,
            fingerprint=fingerprint,
            error=str(exc),
        )
        return None


async def record_external_alert_event(
    *,
    project_id: str,
    provider: str,
    event_id: str,
    stage: str,
    title: str,
    severity: str,
    namespace: str | None = None,
    target_resource: str | None = None,
    fingerprint: str | None = None,
    incident_id: str | None = None,
    approval_id: str | None = None,
    source_url: str | None = None,
    labels: dict[str, Any] | None = None,
    annotations: dict[str, Any] | None = None,
    payload: dict[str, Any] | None = None,
    is_duplicate: bool = False,
) -> UUID | None:
    """
    將 Sentry / SignOz 等非 Alertmanager 告警鏡像到 conversation_event。

    這是 truth-chain 的最低共用入口：只寫 redacted content + source_envelope，
    不改變原本 webhook 的通知、審批或自動化行為。
    """
    provider_name = str(provider or "external").strip().lower() or "external"
    provider_event_id = build_external_alert_provider_event_id(provider_name, event_id, stage)
    content = "\n".join([
        f"{provider_name} inbound {stage}",
        f"Event ID: {event_id}",
        f"Title: {title}",
        f"Severity: {severity}",
        f"Namespace: {namespace or '-'}",
        f"Target: {target_resource or '-'}",
        f"Fingerprint: {fingerprint or '-'}",
        f"Incident: {incident_id or '-'}",
        f"Approval: {approval_id or '-'}",
        f"Source URL: {source_url or '-'}",
    ])
    source_envelope = build_inbound_source_envelope(
        provider=provider_name,
        stage=stage,
        provider_event_id=provider_event_id,
        raw_event_id=event_id,
        raw_content=content,
        alertname=title,
        severity=severity,
        namespace=namespace,
        target_resource=target_resource,
        fingerprint=fingerprint,
        incident_id=str(incident_id) if incident_id else None,
        approval_id=str(approval_id) if approval_id else None,
        source_url=source_url,
        labels=labels,
        annotations=annotations,
        extra={
            "payload": payload or {},
        },
    )

    try:
        from src.db.base import get_db_context

        async with get_db_context(project_id) as db:
            run_id = build_external_alert_run_id(project_id, provider_event_id)
            await ensure_completed_shadow_run(
                db,
                project_id=project_id,
                run_id=run_id,
                agent_id=f"legacy-{provider_name}-webhook",
                trigger_type=f"{provider_name}_inbound",
                trigger_ref=provider_event_id,
                input_payload={
                    "provider": provider_name,
                    "event_id": event_id,
                    "stage": stage,
                    "severity": severity,
                    "namespace": namespace,
                    "target_resource": target_resource,
                    "fingerprint": fingerprint,
                    "incident_id": str(incident_id) if incident_id else None,
                    "approval_id": str(approval_id) if approval_id else None,
                },
            )
            event_uuid = await mirror_inbound_event(
                db,
                project_id=project_id,
                channel_type="internal",
                provider_event_id=provider_event_id,
                platform_subject_id=provider_name,
                channel_user_id=provider_name,
                channel_chat_id=f"{provider_name}:{namespace or 'default'}",
                content_type="text",
                raw_content=content,
                source_envelope=source_envelope,
                provider_ts=_db_timestamp_now(),
                run_id=run_id,
                is_duplicate=is_duplicate,
            )

        logger.info(
            "external_alert_event_recorded",
            project_id=project_id,
            provider=provider_name,
            event_id=event_id,
            stage=stage,
            conversation_event_id=str(event_uuid),
            incident_id=str(incident_id) if incident_id else None,
            approval_id=str(approval_id) if approval_id else None,
        )
        return event_uuid
    except Exception as exc:
        logger.warning(
            "external_alert_event_record_failed",
            project_id=project_id,
            provider=provider_name,
            event_id=event_id,
            stage=stage,
            error=str(exc),
        )
        return None


# ─────────────────────────────────────────────────────────────────────────────
# 出站訊息記錄
# ─────────────────────────────────────────────────────────────────────────────

async def record_outbound_message(
    db: AsyncSession,
    *,
    project_id: str,
    run_id: UUID,
    channel_type: str,
    channel_chat_id: str,
    message_type: str,           # 'interim' | 'final' | 'error' | 'approval_request'
    content: str | None = None,
    source_envelope: dict[str, Any] | None = None,
    provider_message_id: str | None = None,
    send_status: str = "pending",
    conversation_event_id: UUID | None = None,
    triggered_by_state: str | None = None,
    waiting_since: datetime | None = None,
    is_shadow: bool = True,
) -> UUID:
    """
    記錄出站訊息到 awooop_outbound_message。

    is_shadow=True：status='shadow'（不實際發送，只記錄）
    """
    content_hash: str | None = None
    content_preview: str | None = None
    content_redacted: str | None = None
    if content is not None:
        content_hash = hashlib.sha256(content.encode()).hexdigest()
        content_redacted = _redact_string(content)
        content_preview = content_redacted[:256]

    envelope: dict[str, Any] = sanitize(source_envelope or {})
    envelope.update({
        "schema_version": "outbound_source_envelope_v1",
        "redaction_version": _OUTBOUND_REDACTION_VERSION,
        "content_sha256": content_hash,
        "content_length": len(content) if content is not None else 0,
    })
    source_envelope_json = json.dumps(envelope, ensure_ascii=False, default=str)

    actual_status = "shadow" if is_shadow else send_status
    sent_at = (
        _db_timestamp_now()
        if actual_status == "sent"
        else None
    )

    await ensure_completed_shadow_run(
        db,
        project_id=project_id,
        run_id=run_id,
        agent_id="legacy-telegram-gateway",
        trigger_type="legacy_outbound",
        trigger_ref=provider_message_id,
        input_payload={
            "channel_type": channel_type,
            "channel_chat_id": channel_chat_id,
            "message_type": message_type,
            "send_status": actual_status,
            "triggered_by_state": triggered_by_state,
        },
    )

    result = await db.execute(
        text("""
            INSERT INTO awooop_outbound_message (
                project_id, run_id, conversation_event_id,
                channel_type, channel_chat_id, message_type,
                content_hash, content_preview, content_redacted,
                redaction_version, source_envelope,
                provider_message_id,
                send_status, queued_at, sent_at,
                triggered_by_state, waiting_since
            ) VALUES (
                :project_id, :run_id, :conversation_event_id,
                :channel_type, :channel_chat_id, :message_type,
                :content_hash, :content_preview, :content_redacted,
                :redaction_version, CAST(:source_envelope AS jsonb),
                :provider_message_id,
                :send_status, NOW(), :sent_at,
                :triggered_by_state, :waiting_since
            )
            RETURNING message_id
        """),
        {
            "project_id": project_id,
            "run_id": run_id,
            "conversation_event_id": conversation_event_id,
            "channel_type": channel_type,
            "channel_chat_id": channel_chat_id,
            "message_type": message_type,
            "content_hash": content_hash,
            "content_preview": content_preview,
            "content_redacted": content_redacted,
            "redaction_version": _OUTBOUND_REDACTION_VERSION,
            "source_envelope": source_envelope_json,
            "provider_message_id": provider_message_id,
            "send_status": actual_status,
            "sent_at": sent_at,
            "triggered_by_state": triggered_by_state,
            "waiting_since": waiting_since,
        },
    )
    row = result.fetchone()
    message_id: UUID = row[0]
    logger.info(
        "outbound_message_recorded",
        project_id=project_id,
        run_id=str(run_id),
        message_type=message_type,
        send_status=actual_status,
        message_id=str(message_id),
    )
    return message_id


# ─────────────────────────────────────────────────────────────────────────────
# Progressive Feedback Policy
# ─────────────────────────────────────────────────────────────────────────────

async def schedule_interim_feedback(
    *,
    project_id: str,
    run_id: UUID,
    channel_type: str,
    channel_chat_id: str,
    conversation_event_id: UUID | None = None,
    is_shadow: bool = True,
    wait_seconds: int = _INTERIM_WAIT_SECONDS,
) -> None:
    """
    Progressive Feedback Policy：
    等待 wait_seconds 秒後，若 run 仍在 WAITING_TOOL → 發 interim 訊息。

    Shadow Mode：記錄到 outbound_message(status='shadow')，不實際發 Telegram 訊息。
    """
    asyncio.create_task(
        _interim_feedback_task(
            project_id=project_id,
            run_id=run_id,
            channel_type=channel_type,
            channel_chat_id=channel_chat_id,
            conversation_event_id=conversation_event_id,
            is_shadow=is_shadow,
            wait_seconds=wait_seconds,
        ),
        name=f"interim_feedback_{str(run_id)[:8]}",
    )


async def _interim_feedback_task(
    *,
    project_id: str,
    run_id: UUID,
    channel_type: str,
    channel_chat_id: str,
    conversation_event_id: UUID | None,
    is_shadow: bool,
    wait_seconds: int,
) -> None:
    """等待後查 run state，仍 waiting_tool 才發 interim"""
    await asyncio.sleep(wait_seconds)

    try:
        from src.db.base import get_db_context

        async with get_db_context(project_id) as db:
            result = await db.execute(
                select(AwoooPRunState.state, AwoooPRunState.is_shadow).where(
                    AwoooPRunState.run_id == run_id,
                    AwoooPRunState.project_id == project_id,
                )
            )
            row = result.first()

            if row is None:
                logger.warning(
                    "interim_feedback_run_not_found",
                    run_id=str(run_id),
                )
                return

            state, run_is_shadow = row
            if state != "waiting_tool":
                # run 已推進（complete/failed 等），不需要 interim
                return

            waiting_since = datetime.now(UTC)
            interim_content = "AI 正在分析中，請稍候... ⏳"

            await record_outbound_message(
                db,
                project_id=project_id,
                run_id=run_id,
                channel_type=channel_type,
                channel_chat_id=channel_chat_id,
                message_type="interim",
                content=interim_content,
                send_status="pending",
                conversation_event_id=conversation_event_id,
                triggered_by_state="waiting_tool",
                waiting_since=waiting_since,
                is_shadow=is_shadow or run_is_shadow,
            )

            if not (is_shadow or run_is_shadow):
                # Non-shadow：實際發 Telegram 訊息
                await _send_telegram_interim(
                    channel_chat_id=channel_chat_id,
                    content=interim_content,
                    run_id=run_id,
                )

            logger.info(
                "interim_feedback_sent",
                project_id=project_id,
                run_id=str(run_id),
                is_shadow=is_shadow or run_is_shadow,
            )

    except Exception as exc:
        logger.exception(
            "interim_feedback_task_error",
            run_id=str(run_id),
            error=str(exc),
        )


async def _send_telegram_interim(
    *,
    channel_chat_id: str,
    content: str,
    run_id: UUID,
) -> None:
    """實際發送 Telegram interim 訊息（non-shadow 專用）"""
    try:
        import os

        import httpx

        bot_token = os.environ.get("TELEGRAM_BOT_TOKEN")
        if not bot_token:
            logger.warning("interim_telegram_no_token", run_id=str(run_id))
            return

        async with httpx.AsyncClient(timeout=10) as client:
            await client.post(
                f"https://api.telegram.org/bot{bot_token}/sendMessage",
                json={
                    "chat_id": channel_chat_id,
                    "text": content,
                    "parse_mode": "HTML",
                },
            )
    except Exception as exc:
        logger.warning(
            "interim_telegram_send_failed",
            run_id=str(run_id),
            error=str(exc),
        )


# ─────────────────────────────────────────────────────────────────────────────
# Channel Hub 主入口（Telegram inbound）
# ─────────────────────────────────────────────────────────────────────────────

async def handle_telegram_inbound(
    db: AsyncSession,
    *,
    project_id: str,
    agent_id: str,
    message_id: str,
    user_id: str,
    chat_id: str,
    text: str | None = None,
    is_shadow: bool = True,
) -> dict[str, Any]:
    """
    Telegram 入站訊息的統一處理入口：
    1. mirror_inbound_event（記錄）
    2. create_run（建立 platform run）
    3. schedule_interim_feedback（Progressive Feedback）
    4. 回傳 {event_id, run_id, is_duplicate}
    """
    # Step 1: 嘗試建立 run（有冪等保護）
    run_id, is_duplicate = await create_run(
        project_id=project_id,
        agent_id=agent_id,
        trigger_type="channel_event",
        trigger_ref=f"telegram:{message_id}",
        input_payload={"chat_id": chat_id, "user_id": user_id},
        channel_type="telegram",
        provider_event_id=message_id,
    )

    # Step 2: Mirror event（含 run_id）
    event_id = await mirror_inbound_event(
        db,
        project_id=project_id,
        channel_type="telegram",
        provider_event_id=message_id,
        channel_user_id=user_id,
        channel_chat_id=chat_id,
        content_type="text" if text else "callback_query",
        raw_content=text,
        run_id=run_id,
        is_duplicate=is_duplicate,
    )

    # Step 3: Progressive Feedback（30s 計時器）
    if not is_duplicate:
        await schedule_interim_feedback(
            project_id=project_id,
            run_id=run_id,
            channel_type="telegram",
            channel_chat_id=chat_id,
            conversation_event_id=event_id,
            is_shadow=is_shadow,
        )

    return {
        "event_id": str(event_id),
        "run_id": str(run_id),
        "is_duplicate": is_duplicate,
    }
