"""Reorg detection and handling.

Implements reorg detection from SPEC 5.2:
- Maintain block_hash_history window
- Compare stored hash at anchor height with chain
- Binary search to find last matching height
- Rewind and reprocess on reorg detection
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

from brawny.logging import LogEvents, get_logger
from brawny.metrics import REORGS_DETECTED, get_metrics
from brawny.model.enums import AttemptStatus, IntentStatus, NonceStatus
from brawny.tx.intent import transition_intent

if TYPE_CHECKING:
    from brawny.db.base import Database
    from brawny.lifecycle import LifecycleDispatcher
    from brawny._rpc.manager import RPCManager

logger = get_logger(__name__)


@dataclass
class ReorgResult:
    """Result of reorg detection."""

    reorg_detected: bool
    reorg_depth: int = 0
    last_good_height: int | None = None
    intents_reverted: int = 0


class ReorgDetector:
    """Reorg detector using block hash history comparison.

    Algorithm:
    1. Select anchor height (last_processed - reorg_depth)
    2. Compare stored hash at anchor to current chain hash
    3. If mismatch, binary search for last matching height
    4. Rewind state and handle affected intents
    """

    def __init__(
        self,
        db: Database,
        rpc: RPCManager,
        chain_id: int,
        reorg_depth: int = 32,
        block_hash_history_size: int = 256,
        lifecycle: "LifecycleDispatcher | None" = None,
        deep_reorg_alert_enabled: bool = True,
    ) -> None:
        """Initialize reorg detector.

        Args:
            db: Database connection
            rpc: RPC manager
            chain_id: Chain ID
            reorg_depth: Blocks back to check for reorg
            block_hash_history_size: Size of hash history window
        """
        self._db = db
        self._rpc = rpc
        self._chain_id = chain_id
        self._reorg_depth = reorg_depth
        self._history_size = block_hash_history_size
        self._lifecycle = lifecycle
        self._deep_reorg_alert_enabled = deep_reorg_alert_enabled

    def check(self, current_block: int) -> ReorgResult:
        """Check for reorg at the current block height.

        Args:
            current_block: Current block being processed

        Returns:
            ReorgResult with detection status
        """
        # Get block state
        block_state = self._db.get_block_state(self._chain_id)
        if block_state is None:
            return ReorgResult(reorg_detected=False)

        last_processed = block_state.last_processed_block_number

        # Calculate anchor height
        anchor_height = max(0, last_processed - self._reorg_depth)

        # Get stored hash at anchor
        stored_hash = self._db.get_block_hash_at_height(self._chain_id, anchor_height)
        if stored_hash is None:
            # No history at anchor - check if we have any history
            oldest_block = self._db.get_oldest_block_in_history(self._chain_id)
            if oldest_block is None:
                return ReorgResult(reorg_detected=False)
            anchor_height = oldest_block
            stored_hash = self._db.get_block_hash_at_height(self._chain_id, anchor_height)
            if stored_hash is None:
                return ReorgResult(reorg_detected=False)

        # Get current chain hash at anchor
        try:
            block = self._rpc.get_block(anchor_height)
            if block is None:
                return ReorgResult(reorg_detected=False)

            chain_hash = block.get("hash")
            if chain_hash is None:
                logger.warning(
                    "reorg.missing_block_hash",
                    block_number=anchor_height,
                )
                return ReorgResult(reorg_detected=False)
            if isinstance(chain_hash, bytes):
                chain_hash = chain_hash.hex()
            if not chain_hash.startswith("0x"):
                chain_hash = f"0x{chain_hash}"
        except Exception as e:
            logger.warning(
                "reorg.check_failed",
                anchor_height=anchor_height,
                error=str(e),
            )
            return ReorgResult(reorg_detected=False)

        # Compare hashes
        stored_normalized = stored_hash.lower()
        chain_normalized = chain_hash.lower()

        if stored_normalized == chain_normalized:
            # No reorg
            return ReorgResult(reorg_detected=False)

        # Reorg detected!
        logger.warning(
            LogEvents.BLOCK_REORG_DETECTED,
            anchor_height=anchor_height,
            stored_hash=stored_hash[:18],
            chain_hash=chain_hash[:18],
        )
        metrics = get_metrics()
        metrics.counter(REORGS_DETECTED).inc(
            chain_id=self._chain_id,
        )

        # Find last good height via binary search
        last_good_height = self._find_last_good_height(anchor_height, last_processed)
        oldest = self._db.get_oldest_block_in_history(self._chain_id)
        if oldest is not None and last_good_height < oldest:
            logger.error(
                LogEvents.BLOCK_REORG_DEEP,
                oldest_known=oldest,
                history_size=self._history_size,
            )
            if self._lifecycle and self._deep_reorg_alert_enabled:
                self._lifecycle.on_deep_reorg(oldest, self._history_size, last_processed)
            return ReorgResult(
                reorg_detected=True,
                reorg_depth=last_processed - (oldest - 1),
                last_good_height=None,
            )

        # Handle impossible state: mismatch at anchor but last_good >= anchor
        # This happens with sparse hash history - delete stale anchor hash
        if last_good_height >= anchor_height:
            logger.warning(
                "reorg.stale_hash_detected",
                anchor_height=anchor_height,
                last_good_height=last_good_height,
                stored_hash=stored_hash[:18],
                chain_hash=chain_hash[:18],
            )
            # Delete the stale hash at anchor and set last_good to anchor - 1
            self._db.delete_block_hash_at_height(self._chain_id, anchor_height)
            last_good_height = anchor_height - 1

        reorg_depth = last_processed - last_good_height

        logger.warning(
            LogEvents.BLOCK_REORG_REWIND,
            last_good_height=last_good_height,
            reorg_depth=reorg_depth,
        )

        return ReorgResult(
            reorg_detected=True,
            reorg_depth=reorg_depth,
            last_good_height=last_good_height,
        )

    def _find_last_good_height(self, low: int, high: int) -> int:
        """Binary search to find last matching block height.

        Args:
            low: Lower bound (known bad)
            high: Upper bound (known bad)

        Returns:
            Last good block height
        """
        oldest = self._db.get_oldest_block_in_history(self._chain_id)
        if oldest is None:
            return low

        # Start from the known bad anchor and search forward
        # We need to find where the chain diverged
        left = max(oldest, low)
        right = high

        last_good = left - 1  # Assume nothing matches if search fails

        while left <= right:
            mid = (left + right) // 2

            stored = self._db.get_block_hash_at_height(self._chain_id, mid)
            if stored is None:
                # No history here, move right
                left = mid + 1
                continue

            try:
                block = self._rpc.get_block(mid)
                if block is None:
                    left = mid + 1
                    continue

                chain_hash = block["hash"]
                if isinstance(chain_hash, bytes):
                    chain_hash = chain_hash.hex()
                if not chain_hash.startswith("0x"):
                    chain_hash = f"0x{chain_hash}"
            except Exception:
                left = mid + 1
                continue

            if stored.lower() == chain_hash.lower():
                # Match - reorg is after this point
                last_good = mid
                left = mid + 1
            else:
                # Mismatch - reorg is at or before this point
                right = mid - 1

        return last_good

    def rewind(self, to_height: int) -> ReorgResult:
        """Rewind state to a specific height.

        Handles:
        - Deleting block hashes above height
        - Updating block state
        - Reverting confirmed intents that were in reorged blocks

        Args:
            to_height: Height to rewind to

        Returns:
            ReorgResult with details of affected intents
        """
        # Delete block hashes above the rewind height
        deleted_hashes = self._db.delete_block_hashes_above(self._chain_id, to_height)

        logger.info(
            "reorg.hashes_deleted",
            to_height=to_height,
            deleted_count=deleted_hashes,
        )

        # Get the hash at the rewind height for state update
        rewind_hash = self._db.get_block_hash_at_height(self._chain_id, to_height)
        if rewind_hash is None:
            # Try to get from chain
            try:
                block = self._rpc.get_block(to_height)
                if block:
                    rewind_hash = block["hash"]
                    if isinstance(rewind_hash, bytes):
                        rewind_hash = rewind_hash.hex()
            except Exception:
                rewind_hash = None

        if rewind_hash is None:
            logger.warning(
                "reorg.rewind_hash_missing",
                to_height=to_height,
            )
            rewind_hash = "0x0"  # Fallback sentinel

        # Update block state
        self._db.upsert_block_state(self._chain_id, to_height, rewind_hash or "0x0")

        # Revert confirmed intents that were confirmed in reorged blocks
        intents_reverted = self._revert_reorged_intents(to_height)

        return ReorgResult(
            reorg_detected=True,
            last_good_height=to_height,
            intents_reverted=intents_reverted,
        )

    def _revert_reorged_intents(self, to_height: int) -> int:
        """Revert intents that were confirmed in blocks above the rewind height.

        Args:
            to_height: Rewind height

        Returns:
            Number of intents reverted
        """
        # Get confirmed intents
        confirmed_intents = self._db.get_intents_by_status(
            IntentStatus.CONFIRMED.value,
            chain_id=self._chain_id,
        )

        reverted = 0
        for intent in confirmed_intents:
            # Get the latest attempt
            attempt = self._db.get_latest_attempt_for_intent(intent.intent_id)
            if attempt is None:
                continue

            # Check if attempt was confirmed in a reorged block
            if attempt.included_block and attempt.included_block > to_height:
                # This intent was confirmed in a reorged block
                # Revert to pending status

                transition_intent(
                    self._db,
                    intent.intent_id,
                    IntentStatus.PENDING,
                    "reorg_revert",
                    chain_id=self._chain_id,
                )

                self._db.update_attempt_status(
                    attempt.attempt_id,
                    AttemptStatus.PENDING.value,
                )
                try:
                    signer_address = intent.signer_address.lower()
                    reservation = self._db.get_nonce_reservation(
                        self._chain_id,
                        signer_address,
                        attempt.nonce,
                    )
                    if reservation is None:
                        self._db.create_nonce_reservation(
                            self._chain_id,
                            signer_address,
                            attempt.nonce,
                            status=NonceStatus.IN_FLIGHT.value,
                            intent_id=intent.intent_id,
                        )
                    else:
                        self._db.update_nonce_reservation_status(
                            self._chain_id,
                            signer_address,
                            attempt.nonce,
                            NonceStatus.IN_FLIGHT.value,
                            intent_id=intent.intent_id,
                        )
                except Exception as e:
                    logger.warning(
                        "reorg.nonce_reconcile_failed",
                        intent_id=str(intent.intent_id),
                        nonce=attempt.nonce,
                        error=str(e)[:200],
                    )

                if self._lifecycle:
                    self._lifecycle.on_reorged(intent, attempt, to_height)

                logger.warning(
                    LogEvents.INTENT_REORG,
                    intent_id=str(intent.intent_id),
                    attempt_id=str(attempt.attempt_id),
                    old_block=attempt.included_block,
                    reorg_height=to_height,
                )

                reverted += 1

        return reverted

    def handle_deep_reorg(self) -> None:
        """Handle a reorg deeper than our history window.

        This is a critical situation - emit error and rewind to oldest known block.
        """
        oldest = self._db.get_oldest_block_in_history(self._chain_id)

        logger.error(
            LogEvents.BLOCK_REORG_DEEP,
            oldest_known=oldest,
            history_size=self._history_size,
        )

        if oldest is not None:
            self.rewind(oldest)
