"""Simplified lifecycle dispatcher for job hooks and alerts.

Implements 3 alert hooks (triggered, confirmed, failed) and 2 callbacks
(on_success, on_failure). All failure modes route through on_failed().
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any
from uuid import UUID

from brawny.alerts.send import AlertConfig, AlertEvent, AlertPayload
from brawny.jobs.kv import DatabaseJobKVStore
from brawny.logging import LogEvents, get_logger
from brawny.model.contexts import AlertContext, BlockContext
from brawny.model.errors import (
    ErrorInfo,
    FailureStage,
    FailureType,
    HookType,
    TriggerReason,
)
from brawny.model.events import DecodedEvent
from brawny.model.types import BlockInfo, Trigger

if TYPE_CHECKING:
    from brawny.config import Config
    from brawny.db.base import Database
    from brawny.jobs.base import Job, TxInfo, TxReceipt, BlockInfo as AlertBlockInfo
    from brawny.model.types import TxAttempt, TxIntent
    from brawny._rpc.manager import RPCManager
    from brawny.alerts.contracts import ContractSystem, SimpleContractFactory

logger = get_logger(__name__)


class LifecycleDispatcher:
    """Dispatch job lifecycle hooks and alert notifications.

    Alert Hooks (3):
        - alert_triggered: Job check returns Trigger
        - alert_confirmed: Transaction confirmed on-chain
        - alert_failed: Any terminal failure

    Callbacks (2):
        - on_success: After confirmation (internal use)
        - on_failure: After failure (internal use)
    """

    def __init__(
        self,
        db: Database,
        rpc: RPCManager,
        config: Config,
        jobs: dict[str, Job],
        contract_system: ContractSystem | None = None,
    ) -> None:
        self._db = db
        self._rpc = rpc
        self._config = config
        self._jobs = jobs
        self._contract_system = contract_system
        self._global_alert_config = self._build_global_alert_config()

    # =========================================================================
    # Public API
    # =========================================================================

    def on_triggered(
        self,
        job: Job,
        trigger: Trigger,
        block: BlockInfo,
        intent_id: UUID | None = None,
    ) -> None:
        """Called when job check returns a Trigger."""
        if intent_id:
            self._store_trigger(job.job_id, intent_id, trigger)
        self._send_alert(
            job,
            HookType.TRIGGERED,
            trigger=trigger,
            block=self._to_alert_block(block),
            intent_id=intent_id,
        )

    def on_submitted(self, intent: TxIntent, attempt: TxAttempt) -> None:
        """Log submission for observability. No job hook."""
        logger.info(
            "tx.submitted",
            intent_id=str(intent.intent_id),
            attempt_id=str(attempt.attempt_id),
            tx_hash=attempt.tx_hash,
            nonce=attempt.nonce,
            job_id=intent.job_id,
            chain_id=self._config.chain_id,
        )

    def on_confirmed(
        self,
        intent: TxIntent,
        attempt: TxAttempt,
        receipt: dict[str, Any],
    ) -> None:
        """Called when transaction is confirmed on-chain."""
        job = self._jobs.get(intent.job_id)
        if not job:
            return
        trigger = self._load_trigger(job.job_id, intent.intent_id)
        alert_receipt = self._build_alert_receipt(receipt)
        self._run_callback(job, "on_success", intent, attempt, alert_receipt)
        self._send_alert(
            job,
            HookType.CONFIRMED,
            trigger=trigger,
            intent=intent,
            attempt=attempt,
            receipt=receipt,
            intent_id=intent.intent_id,
        )
        self._cleanup_trigger(job.job_id, intent.intent_id)

    def on_failed(
        self,
        intent: TxIntent,
        attempt: TxAttempt | None,
        error: Exception,
        failure_type: FailureType,
    ) -> None:
        """Called on any terminal failure. Error is required."""
        job = self._jobs.get(intent.job_id)
        if not job:
            return
        trigger = self._load_trigger(job.job_id, intent.intent_id)
        self._run_callback(job, "on_failure", intent, attempt, error)
        self._send_alert(
            job,
            HookType.FAILED,
            trigger=trigger,
            intent=intent,
            attempt=attempt,
            error_info=ErrorInfo.from_exception(error),
            failure_type=failure_type,
            intent_id=intent.intent_id,
        )
        self._cleanup_trigger(job.job_id, intent.intent_id)

    def on_check_failed(
        self,
        job: Job,
        error: Exception,
        block: BlockInfo,
    ) -> None:
        """Called when job.check() raises an exception. No intent exists."""
        trigger = Trigger(reason=FailureType.CHECK_EXCEPTION.value, data={})
        self._send_alert(
            job,
            HookType.FAILED,
            trigger=trigger,
            error_info=ErrorInfo.from_exception(error),
            failure_type=FailureType.CHECK_EXCEPTION,
            block=self._to_alert_block(block),
            intent_id=None,
        )

    def on_build_tx_failed(
        self,
        job: Job,
        trigger: Trigger,
        error: Exception,
        block: BlockInfo,
    ) -> None:
        """Called when job.build_tx() raises an exception. No intent exists."""
        self._send_alert(
            job,
            HookType.FAILED,
            trigger=trigger,
            error_info=ErrorInfo.from_exception(error),
            failure_type=FailureType.BUILD_TX_EXCEPTION,
            block=self._to_alert_block(block),
            intent_id=None,
        )

    def on_deep_reorg(
        self, oldest_known: int | None, history_size: int, last_processed: int
    ) -> None:
        """System-level alert for deep reorg. Not job-specific."""
        if not self._has_alert_config():
            return
        message = (
            f"Deep reorg detected. History window is insufficient "
            f"to safely verify the chain.\n"
            f"oldest_known={oldest_known}, history_size={history_size}, "
            f"last_processed={last_processed}"
        )
        payload = AlertPayload(
            job_id="system",
            job_name="Deep Reorg",
            event_type=AlertEvent.FAILED,
            message=message,
            chain_id=self._config.chain_id,
        )
        self._fire_alert(payload, self._global_alert_config)

    # =========================================================================
    # Trigger Persistence (Versioned)
    # =========================================================================

    def _store_trigger(self, job_id: str, intent_id: UUID, trigger: Trigger) -> None:
        """Store trigger with version envelope for future evolution."""
        self._db.set_job_kv(
            job_id,
            f"trigger:{intent_id}",
            {
                "v": 1,
                "trigger": {
                    "reason": trigger.reason,
                    "data": trigger.data,
                    "tx_required": trigger.tx_required,
                },
            },
        )

    def _load_trigger(self, job_id: str, intent_id: UUID) -> Trigger:
        """Load trigger, handling missing or unknown versions gracefully."""
        data = self._db.get_job_kv(job_id, f"trigger:{intent_id}")

        if data is None:
            logger.warning("trigger.missing", job_id=job_id, intent_id=str(intent_id))
            return Trigger(reason=TriggerReason.MISSING, data={})

        # Versioned format
        if isinstance(data, dict) and "v" in data:
            v = data["v"]
            if v == 1:
                t = data.get("trigger", {})
                return Trigger(
                    reason=t.get("reason", ""),
                    data=t.get("data", {}),
                    tx_required=bool(t.get("tx_required", True)),
                )
            logger.warning("trigger.unknown_version", version=v, job_id=job_id)
            return Trigger(reason=TriggerReason.UNKNOWN_VERSION, data={})

        # Legacy format (no version)
        if isinstance(data, dict) and "reason" in data:
            return Trigger(
                reason=data.get("reason", ""),
                data=data.get("data", {}),
                tx_required=bool(data.get("tx_required", True)),
            )

        return Trigger(reason=TriggerReason.INVALID_FORMAT, data={})

    def _cleanup_trigger(self, job_id: str, intent_id: UUID) -> None:
        """Delete trigger data from job KV after intent reaches terminal state."""
        self._db.delete_job_kv(job_id, f"trigger:{intent_id}")

    # For backward compatibility with callers using old names
    def record_trigger(self, job_id: str, intent_id: UUID, trigger: Trigger) -> None:
        """Alias for _store_trigger. Use _store_trigger for new code."""
        self._store_trigger(job_id, intent_id, trigger)

    def load_trigger(self, job_id: str, intent_id: UUID) -> Trigger:
        """Alias for _load_trigger. Use _load_trigger for new code."""
        return self._load_trigger(job_id, intent_id)

    # =========================================================================
    # Alert Dispatch
    # =========================================================================

    def _send_alert(
        self,
        job: Job,
        hook_type: HookType,
        *,
        trigger: Trigger,
        intent: TxIntent | None = None,
        attempt: TxAttempt | None = None,
        receipt: dict[str, Any] | None = None,
        error_info: ErrorInfo | None = None,
        failure_type: FailureType | None = None,
        block: AlertBlockInfo | None = None,
        intent_id: UUID | None = None,
    ) -> None:
        """Build context and dispatch to job's alert hook."""
        hook_fn = getattr(job, f"alert_{hook_type.value}", None)
        if hook_fn is None:
            return

        # Derive block based on hook_type
        if hook_type == HookType.TRIGGERED:
            alert_block = block
        elif hook_type == HookType.CONFIRMED and receipt:
            alert_block = self._fetch_block(receipt.get("blockNumber"))
        elif hook_type == HookType.FAILED:
            alert_block = self._get_block_for_failed(attempt, receipt) or block
        else:
            alert_block = None

        # Receipt only exposed for confirmed
        ctx_receipt = (
            self._build_alert_receipt(receipt)
            if (hook_type == HookType.CONFIRMED and receipt)
            else None
        )

        # Enforce error_info guarantee for failed
        if hook_type == HookType.FAILED and error_info is None:
            logger.error("alert.failed_missing_error_info", job_id=job.job_id)
            error_info = ErrorInfo(error_type="UnknownError", message="unknown")

        # Build BlockContext from alert block
        block_ctx = self._to_block_context(alert_block)

        # Build ContractFactory
        from brawny.alerts.contracts import SimpleContractFactory

        contracts = SimpleContractFactory(self._contract_system) if self._contract_system else None

        # Decode events for confirmed hook
        events: list[DecodedEvent] | None = None
        if hook_type == HookType.CONFIRMED and ctx_receipt and self._contract_system:
            events = self._decode_receipt_events(ctx_receipt)

        # Build new AlertContext (OE7)
        ctx = AlertContext(
            block=block_ctx,
            trigger=trigger,
            tx=self._build_tx_info(intent, attempt),
            receipt=ctx_receipt,
            events=events,
            failure_type=failure_type,
            error_info=error_info,
            log=logger.bind(job_id=job.job_id, chain_id=self._config.chain_id),
            contracts=contracts,
            kv=DatabaseJobKVStore(self._db, job.job_id),  # Read-only for alerts
        )

        # Call hook
        from brawny.scripting import set_job_context
        from brawny._context import set_alert_context

        try:
            set_job_context(True)
            set_alert_context(ctx)
            message = hook_fn(ctx)
        except Exception as e:
            logger.error(
                "alert.hook_crashed",
                job_id=job.job_id,
                hook=hook_type.value,
                error=str(e)[:200],
            )
            if self._has_alert_config() and self._config.alert_hook_errors:
                self._send_hook_error_alert(job.job_id, hook_type.value, e)
            return
        finally:
            set_job_context(False)
            set_alert_context(None)

        if not message:
            return

        # Deliver alert via new system
        self._deliver_alert(job, message, hook_type)

    def _deliver_alert(
        self,
        job: Job,
        message: str | tuple[str, str],
        hook_type: HookType,
    ) -> None:
        """Deliver alert to appropriate transports."""
        # Extract message text (ignore parse_mode tuple for now)
        msg_text = message[0] if isinstance(message, tuple) else message

        # Map HookType to AlertEvent
        event_map = {
            HookType.TRIGGERED: AlertEvent.TRIGGERED,
            HookType.CONFIRMED: AlertEvent.CONFIRMED,
            HookType.FAILED: AlertEvent.FAILED,
        }
        event_type = event_map.get(hook_type, AlertEvent.FAILED)

        payload = AlertPayload(
            job_id=job.job_id,
            job_name=job.name,
            event_type=event_type,
            message=msg_text,
            chain_id=self._config.chain_id,
        )

        # Get job-specific or global config
        alert_config = self._get_alert_config_for_job(job)
        self._fire_alert(payload, alert_config)

    def _send_hook_error_alert(self, job_id: str, hook_type: str, error: Exception) -> None:
        """Send fallback error alert when a hook fails."""
        message = f"Alert hook failed for job {job_id}: {error}"
        payload = AlertPayload(
            job_id=job_id,
            job_name=job_id,
            event_type=AlertEvent.FAILED,
            message=message,
            chain_id=self._config.chain_id,
        )
        self._fire_alert(payload, self._global_alert_config)

    def _fire_alert(self, payload: AlertPayload, config: AlertConfig) -> None:
        """Fire alert asynchronously. Fire-and-forget."""
        import asyncio
        from brawny.alerts import send as alerts_send

        try:
            loop = asyncio.get_running_loop()
            loop.create_task(alerts_send.send_alert(payload, config))
        except RuntimeError:
            # No running loop - run synchronously
            asyncio.run(alerts_send.send_alert(payload, config))

    def _build_global_alert_config(self) -> AlertConfig:
        """Build global AlertConfig from application config."""
        chat_ids: list[str] = []
        if self._config.telegram_chat_id:
            chat_ids.append(self._config.telegram_chat_id)

        return AlertConfig(
            telegram_token=self._config.telegram_bot_token,
            telegram_chat_ids=chat_ids,
        )

    def _get_alert_config_for_job(self, job: Job) -> AlertConfig:
        """Resolve per-job overrides into job-scoped AlertConfig."""
        job_chat_ids = getattr(job, "telegram_chat_ids", None)
        if job_chat_ids:
            # Job-level targets override global
            return AlertConfig(
                telegram_token=self._config.telegram_bot_token,
                telegram_chat_ids=list(job_chat_ids),
            )
        return self._global_alert_config

    def _has_alert_config(self) -> bool:
        """Check if any alert transport is configured."""
        return bool(
            self._global_alert_config.telegram_token
            and self._global_alert_config.telegram_chat_ids
        )

    # =========================================================================
    # Callbacks (on_success, on_failure)
    # =========================================================================

    def _run_callback(
        self,
        job: Job,
        callback_name: str,
        intent: TxIntent,
        attempt: TxAttempt | None,
        result: Any,  # TxReceipt for success, Exception for failure
    ) -> None:
        """Run job callback (on_success or on_failure)."""
        from brawny.scripting import set_job_context

        callback_fn = getattr(job, callback_name, None)
        if callback_fn is None:
            return

        try:
            block_info = self._get_callback_block(attempt)
            trigger = self._load_trigger(job.job_id, intent.intent_id)
            ctx = self._build_alert_context_for_callback(
                job, block_info, trigger, intent, attempt, result, callback_name
            )
            set_job_context(True)
            if callback_name == "on_success":
                callback_fn(ctx, result, intent, attempt)
            else:
                callback_fn(ctx, result, intent, attempt)
        except Exception as e:
            logger.error(
                f"job.{callback_name}_failed",
                job_id=job.job_id,
                error=str(e)[:200],
            )
            if self._has_alert_config():
                self._send_hook_error_alert(job.job_id, callback_name, e)
        finally:
            set_job_context(False)

    def _build_alert_context_for_callback(
        self,
        job: Job,
        block: BlockInfo | None,
        trigger: Trigger,
        intent: TxIntent,
        attempt: TxAttempt | None,
        result: Any,
        callback_name: str,
    ) -> AlertContext:
        """Build AlertContext for callbacks (on_success, on_failure)."""
        from brawny.alerts.contracts import SimpleContractFactory

        # Build block context
        if block is None:
            block = BlockInfo(
                chain_id=self._config.chain_id,
                block_number=self._rpc.get_block_number(),
                block_hash="0x0",
                timestamp=0,
            )
        block_ctx = BlockContext(
            number=block.block_number,
            timestamp=block.timestamp,
            hash=block.block_hash,
            base_fee=0,
            chain_id=block.chain_id,
        )

        contracts = SimpleContractFactory(self._contract_system) if self._contract_system else None

        # Receipt for success, error_info for failure
        ctx_receipt = result if callback_name == "on_success" else None
        error_info = ErrorInfo.from_exception(result) if callback_name == "on_failure" and isinstance(result, Exception) else None

        return AlertContext(
            block=block_ctx,
            trigger=trigger,
            tx=self._build_tx_info(intent, attempt),
            receipt=ctx_receipt,
            events=None,  # TODO: decode if needed
            failure_type=None,  # Set by caller if needed
            error_info=error_info,
            log=logger.bind(job_id=job.job_id, chain_id=self._config.chain_id),
            contracts=contracts,
            kv=DatabaseJobKVStore(self._db, job.job_id),
        )

    def _get_callback_block(self, attempt: TxAttempt | None) -> BlockInfo | None:
        """Get block info for callback context."""
        if attempt and attempt.broadcast_block:
            return self._model_block_from_number(attempt.broadcast_block)
        return None

    # =========================================================================
    # Helpers
    # =========================================================================

    def _build_tx_info(
        self, intent: TxIntent | None, attempt: TxAttempt | None
    ) -> TxInfo | None:
        """Build TxInfo from intent, enrich with attempt if available."""
        if intent is None:
            return None

        from brawny.jobs.base import TxInfo

        # Safe access for optional gas_params
        gp = getattr(attempt, "gas_params", None) if attempt else None

        return TxInfo(
            hash=attempt.tx_hash if attempt else None,
            nonce=attempt.nonce if attempt else None,
            from_address=intent.signer_address,
            to_address=intent.to_address,
            gas_limit=gp.gas_limit if gp else getattr(intent, "gas_limit", 0),
            max_fee_per_gas=gp.max_fee_per_gas if gp else getattr(intent, "max_fee_per_gas", 0),
            max_priority_fee_per_gas=gp.max_priority_fee_per_gas if gp else getattr(intent, "max_priority_fee_per_gas", 0),
        )

    def _build_alert_receipt(self, receipt: dict[str, Any]) -> TxReceipt:
        """Convert raw receipt dict to TxReceipt."""
        from brawny.jobs.base import TxReceipt

        tx_hash = receipt.get("transactionHash")
        if hasattr(tx_hash, "hex"):
            tx_hash = f"0x{tx_hash.hex()}"
        block_hash = receipt.get("blockHash")
        if hasattr(block_hash, "hex"):
            block_hash = f"0x{block_hash.hex()}"
        return TxReceipt(
            transaction_hash=tx_hash,
            block_number=receipt.get("blockNumber"),
            block_hash=block_hash,
            status=receipt.get("status", 1),
            gas_used=receipt.get("gasUsed", 0),
            logs=receipt.get("logs", []),
        )

    def _get_block_for_failed(
        self,
        attempt: TxAttempt | None,
        receipt: dict[str, Any] | None,
    ) -> AlertBlockInfo | None:
        """Determine block for failed alert. Explicit priority."""
        if receipt and "blockNumber" in receipt:
            return self._fetch_block(receipt["blockNumber"])
        if attempt and attempt.broadcast_block:
            return self._fetch_block(attempt.broadcast_block)
        return None

    def _fetch_block(self, block_number: int | None) -> AlertBlockInfo | None:
        """Fetch block info by number."""
        if block_number is None:
            return None
        try:
            block = self._rpc.get_block(block_number)
        except Exception:
            return None
        return self._to_alert_block(
            BlockInfo(
                chain_id=self._config.chain_id,
                block_number=block["number"],
                block_hash=f"0x{block['hash'].hex()}"
                if hasattr(block["hash"], "hex")
                else block["hash"],
                timestamp=block["timestamp"],
            )
        )

    def _model_block_from_number(self, block_number: int) -> BlockInfo | None:
        """Get BlockInfo model from block number."""
        try:
            block = self._rpc.get_block(block_number)
        except Exception:
            return None
        return BlockInfo(
            chain_id=self._config.chain_id,
            block_number=block["number"],
            block_hash=f"0x{block['hash'].hex()}"
            if hasattr(block["hash"], "hex")
            else block["hash"],
            timestamp=block["timestamp"],
        )

    def _to_alert_block(self, block: BlockInfo) -> AlertBlockInfo:
        """Convert model BlockInfo to alert BlockInfo."""
        from brawny.jobs.base import BlockInfo as AlertBlockInfo

        return AlertBlockInfo(
            number=block.block_number,
            hash=block.block_hash,
            timestamp=block.timestamp,
        )

    def _to_block_context(self, alert_block: AlertBlockInfo | None) -> BlockContext:
        """Convert alert BlockInfo to BlockContext."""
        if alert_block is None:
            # Default block context when no block available
            return BlockContext(
                number=0,
                timestamp=0,
                hash="0x0",
                base_fee=0,
                chain_id=self._config.chain_id,
            )
        return BlockContext(
            number=alert_block.number,
            timestamp=alert_block.timestamp,
            hash=alert_block.hash,
            base_fee=0,  # Not always available in alert context
            chain_id=self._config.chain_id,
        )

    def _decode_receipt_events(self, receipt: TxReceipt) -> list[DecodedEvent]:
        """Decode events from receipt using contract system."""
        if self._contract_system is None:
            return []

        try:
            from brawny.alerts.events import decode_logs

            event_dict = decode_logs(
                logs=receipt.logs,
                contract_system=self._contract_system,
            )
            # Convert EventDict to list[DecodedEvent]
            events: list[DecodedEvent] = []
            for event_name, event_item in event_dict.items():
                # event_item is _EventItem
                # Use getattr with fallbacks for robustness
                args_list = getattr(event_item, "_events", None) or []
                addr_list = getattr(event_item, "_addresses", None) or []
                pos_list = getattr(event_item, "pos", None) or []

                for i, args in enumerate(args_list):
                    address = addr_list[i] if i < len(addr_list) else ""
                    log_index = pos_list[i] if i < len(pos_list) else 0

                    events.append(
                        DecodedEvent.create(
                            address=address,
                            event_name=event_name,
                            args=args,
                            log_index=log_index,
                            tx_hash=receipt.transactionHash,
                            block_number=receipt.blockNumber,
                        )
                    )
            return events
        except Exception as e:
            logger.warning("events.decode_failed", error=str(e)[:200])
            return []
