import hashlib
import logging
import sys
from datetime import datetime, timezone
from typing import Any

from fastapi import APIRouter
from meshcore import EventType
from pydantic import BaseModel, Field

from app.config import get_recent_log_lines, settings
from app.radio_sync import get_contacts_selected_for_radio_sync, get_radio_channel_limit
from app.repository import MessageRepository, StatisticsRepository
from app.routers.health import HealthResponse, build_health_data
from app.services.radio_runtime import radio_runtime
from app.version_info import get_app_build_info, git_output

logger = logging.getLogger(__name__)

router = APIRouter(tags=["debug"])

LOG_COPY_BOUNDARY_MESSAGE = "STOP COPYING HERE IF YOU DO NOT WANT TO INCLUDE LOGS BELOW"
LOG_COPY_BOUNDARY_LINE = "-" * 64
LOG_COPY_BOUNDARY_PREFIX = [
    LOG_COPY_BOUNDARY_LINE,
    LOG_COPY_BOUNDARY_LINE,
    LOG_COPY_BOUNDARY_LINE,
    LOG_COPY_BOUNDARY_LINE,
    LOG_COPY_BOUNDARY_MESSAGE,
    LOG_COPY_BOUNDARY_LINE,
    LOG_COPY_BOUNDARY_LINE,
    LOG_COPY_BOUNDARY_LINE,
    LOG_COPY_BOUNDARY_LINE,
]


class DebugApplicationInfo(BaseModel):
    version: str
    version_source: str
    commit_hash: str | None = None
    commit_source: str | None = None
    git_branch: str | None = None
    git_dirty: bool | None = None
    python_version: str


class DebugRuntimeInfo(BaseModel):
    connection_info: str | None = None
    connection_desired: bool
    setup_in_progress: bool
    setup_complete: bool
    channels_with_incoming_messages: int
    max_channels: int
    path_hash_mode: int
    path_hash_mode_supported: bool
    channel_slot_reuse_enabled: bool
    channel_send_cache_capacity: int
    remediation_flags: dict[str, bool]


class DebugContactAudit(BaseModel):
    expected_and_found: int
    expected_but_not_found: list[str]
    found_but_not_expected: list[str]


class DebugChannelSlotMismatch(BaseModel):
    slot_number: int
    expected_sha256_of_room_key: str | None = None
    actual_sha256_of_room_key: str | None = None


class DebugChannelAudit(BaseModel):
    matched_slots: int
    wrong_slots: list[DebugChannelSlotMismatch]


class DebugRadioProbe(BaseModel):
    performed: bool
    errors: list[str] = Field(default_factory=list)
    multi_acks_enabled: bool | None = None
    self_info: dict[str, Any] | None = None
    device_info: dict[str, Any] | None = None
    stats_core: dict[str, Any] | None = None
    stats_radio: dict[str, Any] | None = None
    contacts: DebugContactAudit | None = None
    channels: DebugChannelAudit | None = None


class DebugDatabaseInfo(BaseModel):
    total_dms: int
    total_channel_messages: int
    total_outgoing: int


class DebugSnapshotResponse(BaseModel):
    captured_at: str
    application: DebugApplicationInfo
    health: HealthResponse
    runtime: DebugRuntimeInfo
    database: DebugDatabaseInfo
    radio_probe: DebugRadioProbe
    logs: list[str]


def _build_application_info() -> DebugApplicationInfo:
    build_info = get_app_build_info()
    dirty_output = git_output("status", "--porcelain")
    return DebugApplicationInfo(
        version=build_info.version,
        version_source=build_info.version_source,
        commit_hash=build_info.commit_hash,
        commit_source=build_info.commit_source,
        git_branch=git_output("rev-parse", "--abbrev-ref", "HEAD"),
        git_dirty=(dirty_output is not None and dirty_output != ""),
        python_version=sys.version.split()[0],
    )


def _event_type_name(event: Any) -> str:
    event_type = getattr(event, "type", None)
    return getattr(event_type, "name", str(event_type))


def _sha256_hex(value: str) -> str:
    return hashlib.sha256(value.encode("utf-8")).hexdigest()


def _normalize_channel_secret(payload: dict[str, Any]) -> bytes:
    secret = payload.get("channel_secret", b"")
    if isinstance(secret, bytes):
        return secret
    return bytes(secret)


def _is_empty_channel_payload(payload: dict[str, Any]) -> bool:
    name = payload.get("channel_name", "")
    return not name or name == "\x00" * len(name)


def _observed_channel_key(event: Any) -> str | None:
    if getattr(event, "type", None) != EventType.CHANNEL_INFO:
        return None

    payload = event.payload or {}
    if _is_empty_channel_payload(payload):
        return None

    return _normalize_channel_secret(payload).hex().upper()


def _coerce_live_max_channels(device_info: dict[str, Any] | None) -> int | None:
    if not device_info or "max_channels" not in device_info:
        return None
    try:
        return int(device_info["max_channels"])
    except (TypeError, ValueError):
        return None


async def _build_contact_audit(
    observed_contacts_payload: dict[str, dict[str, Any]],
) -> DebugContactAudit:
    expected_contacts = await get_contacts_selected_for_radio_sync()
    expected_keys = {contact.public_key.lower() for contact in expected_contacts}
    observed_keys = {public_key.lower() for public_key in observed_contacts_payload}

    return DebugContactAudit(
        expected_and_found=len(expected_keys & observed_keys),
        expected_but_not_found=sorted(_sha256_hex(key) for key in (expected_keys - observed_keys)),
        found_but_not_expected=sorted(_sha256_hex(key) for key in (observed_keys - expected_keys)),
    )


async def _build_channel_audit(mc: Any, max_channels: int | None = None) -> DebugChannelAudit:
    cache_key_by_slot = {
        slot: channel_key for channel_key, slot in radio_runtime.get_channel_send_cache_snapshot()
    }

    matched_slots = 0
    wrong_slots: list[DebugChannelSlotMismatch] = []
    for slot in range(get_radio_channel_limit(max_channels)):
        event = await mc.commands.get_channel(slot)
        expected_key = cache_key_by_slot.get(slot)
        observed_key = _observed_channel_key(event)
        if expected_key == observed_key:
            matched_slots += 1
            continue
        wrong_slots.append(
            DebugChannelSlotMismatch(
                slot_number=slot,
                expected_sha256_of_room_key=_sha256_hex(expected_key) if expected_key else None,
                actual_sha256_of_room_key=_sha256_hex(observed_key) if observed_key else None,
            )
        )

    return DebugChannelAudit(
        matched_slots=matched_slots,
        wrong_slots=wrong_slots,
    )


async def _probe_radio() -> DebugRadioProbe:
    if not radio_runtime.is_connected:
        return DebugRadioProbe(performed=False, errors=["Radio not connected"])

    errors: list[str] = []
    try:
        async with radio_runtime.radio_operation(
            "debug_support_snapshot",
            suspend_auto_fetch=True,
        ) as mc:
            device_info = None
            stats_core = None
            stats_radio = None

            device_event = await mc.commands.send_device_query()
            if getattr(device_event, "type", None) == EventType.DEVICE_INFO:
                device_info = device_event.payload
            else:
                errors.append(f"send_device_query returned {_event_type_name(device_event)}")

            core_event = await mc.commands.get_stats_core()
            if getattr(core_event, "type", None) == EventType.STATS_CORE:
                stats_core = core_event.payload
            else:
                errors.append(f"get_stats_core returned {_event_type_name(core_event)}")

            radio_event = await mc.commands.get_stats_radio()
            if getattr(radio_event, "type", None) == EventType.STATS_RADIO:
                stats_radio = radio_event.payload
            else:
                errors.append(f"get_stats_radio returned {_event_type_name(radio_event)}")

            contacts_event = await mc.commands.get_contacts()
            observed_contacts_payload: dict[str, dict[str, Any]] = {}
            if getattr(contacts_event, "type", None) != EventType.ERROR:
                observed_contacts_payload = contacts_event.payload or {}
            else:
                errors.append(f"get_contacts returned {_event_type_name(contacts_event)}")

            return DebugRadioProbe(
                performed=True,
                errors=errors,
                multi_acks_enabled=bool(mc.self_info.get("multi_acks", 0))
                if mc.self_info is not None
                else None,
                self_info=dict(mc.self_info or {}),
                device_info=device_info,
                stats_core=stats_core,
                stats_radio=stats_radio,
                contacts=await _build_contact_audit(observed_contacts_payload),
                channels=await _build_channel_audit(
                    mc,
                    max_channels=_coerce_live_max_channels(device_info),
                ),
            )
    except Exception as exc:
        logger.warning("Debug support snapshot radio probe failed: %s", exc, exc_info=True)
        errors.append(str(exc))
        return DebugRadioProbe(performed=False, errors=errors)


@router.get("/debug", response_model=DebugSnapshotResponse)
async def debug_support_snapshot() -> DebugSnapshotResponse:
    """Return a support/debug snapshot with recent logs and live radio state."""
    health_data = await build_health_data(radio_runtime.is_connected, radio_runtime.connection_info)
    statistics = await StatisticsRepository.get_all()
    radio_probe = await _probe_radio()
    channels_with_incoming_messages = (
        await MessageRepository.count_channels_with_incoming_messages()
    )
    return DebugSnapshotResponse(
        captured_at=datetime.now(timezone.utc).isoformat(),
        application=_build_application_info(),
        health=HealthResponse(**health_data),
        runtime=DebugRuntimeInfo(
            connection_info=radio_runtime.connection_info,
            connection_desired=radio_runtime.connection_desired,
            setup_in_progress=radio_runtime.is_setup_in_progress,
            setup_complete=radio_runtime.is_setup_complete,
            channels_with_incoming_messages=channels_with_incoming_messages,
            max_channels=radio_runtime.max_channels,
            path_hash_mode=radio_runtime.path_hash_mode,
            path_hash_mode_supported=radio_runtime.path_hash_mode_supported,
            channel_slot_reuse_enabled=radio_runtime.channel_slot_reuse_enabled(),
            channel_send_cache_capacity=radio_runtime.get_channel_send_cache_capacity(),
            remediation_flags={
                "enable_message_poll_fallback": settings.enable_message_poll_fallback,
                "force_channel_slot_reconfigure": settings.force_channel_slot_reconfigure,
            },
        ),
        database=DebugDatabaseInfo(
            total_dms=statistics["total_dms"],
            total_channel_messages=statistics["total_channel_messages"],
            total_outgoing=statistics["total_outgoing"],
        ),
        radio_probe=radio_probe,
        logs=[*LOG_COPY_BOUNDARY_PREFIX, *get_recent_log_lines(limit=1000)],
    )
