import logging
import re
from hashlib import sha256

from fastapi import APIRouter, BackgroundTasks, HTTPException, Response, status
from pydantic import BaseModel, Field

from app.channel_constants import (
    PUBLIC_CHANNEL_KEY,
    PUBLIC_CHANNEL_NAME,
    is_public_channel_key,
    is_public_channel_name,
)
from app.decoder import parse_packet, try_decrypt_packet_with_channel_key
from app.models import Channel, ChannelDetail, ChannelMessageCounts, ChannelTopSender
from app.packet_processor import create_message_from_decrypted
from app.region_scope import normalize_region_scope
from app.repository import ChannelRepository, MessageRepository, RawPacketRepository
from app.websocket import broadcast_event, broadcast_success

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/channels", tags=["channels"])


def _broadcast_channel_update(channel: Channel) -> None:
    broadcast_event("channel", channel.model_dump())


class CreateChannelRequest(BaseModel):
    name: str = Field(min_length=1, max_length=32)
    key: str | None = Field(
        default=None,
        description="Channel key as hex string (32 chars = 16 bytes). If omitted or name starts with #, key is derived from name hash.",
    )


class BulkCreateHashtagChannelsRequest(BaseModel):
    channel_names: list[str] = Field(
        min_length=1,
        description="List of hashtag room names. Leading # is optional per entry.",
    )
    try_historical: bool = Field(
        default=False,
        description="Attempt one background historical decrypt sweep for the newly added rooms.",
    )


class BulkCreateHashtagChannelsResponse(BaseModel):
    created_channels: list[Channel]
    existing_count: int
    invalid_names: list[str]
    decrypt_started: bool = False
    decrypt_total_packets: int = 0
    message: str


class ChannelFloodScopeOverrideRequest(BaseModel):
    flood_scope_override: str = Field(
        description="Blank clears the override; non-empty values temporarily override flood scope"
    )


class ChannelPathHashModeOverrideRequest(BaseModel):
    path_hash_mode_override: int | None = Field(
        default=None,
        ge=0,
        le=2,
        description="Path hash mode override (0=1-byte, 1=2-byte, 2=3-byte, null = use radio default)",
    )


def _derive_channel_identity(
    requested_name: str,
    request_key: str | None = None,
) -> tuple[str, str, bool]:
    is_hashtag = requested_name.startswith("#")

    if is_public_channel_name(requested_name):
        if request_key:
            try:
                key_bytes = bytes.fromhex(request_key)
                if len(key_bytes) != 16:
                    raise HTTPException(
                        status_code=400,
                        detail="Channel key must be exactly 16 bytes (32 hex chars)",
                    )
            except ValueError:
                raise HTTPException(status_code=400, detail="Invalid hex string for key") from None
            if key_bytes.hex().upper() != PUBLIC_CHANNEL_KEY:
                raise HTTPException(
                    status_code=400,
                    detail=f'"{PUBLIC_CHANNEL_NAME}" must use the canonical Public key',
                )
        return PUBLIC_CHANNEL_KEY, PUBLIC_CHANNEL_NAME, False

    if request_key and not is_hashtag:
        try:
            key_bytes = bytes.fromhex(request_key)
            if len(key_bytes) != 16:
                raise HTTPException(
                    status_code=400, detail="Channel key must be exactly 16 bytes (32 hex chars)"
                )
        except ValueError:
            raise HTTPException(status_code=400, detail="Invalid hex string for key") from None
        key_hex = key_bytes.hex().upper()
        if is_public_channel_key(key_hex):
            raise HTTPException(
                status_code=400,
                detail=f'The canonical Public key may only be used for "{PUBLIC_CHANNEL_NAME}"',
            )
        return key_hex, requested_name, False

    key_bytes = sha256(requested_name.encode("utf-8")).digest()[:16]
    return key_bytes.hex().upper(), requested_name, is_hashtag


def _normalize_bulk_hashtag_name(name: str) -> str | None:
    trimmed = name.strip()
    if not trimmed:
        return None
    normalized = trimmed.lstrip("#").strip()
    if not normalized:
        return None
    if len(normalized) > 31:
        return None
    if not re.fullmatch(r"[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*", normalized):
        return None
    return f"#{normalized}"


async def _run_historical_channel_decryption_for_channels(
    channels: list[tuple[bytes, str, str]],
) -> None:
    total = await RawPacketRepository.get_undecrypted_count()
    decrypted_count = 0
    matched_channel_names: set[str] = set()

    if total == 0:
        logger.info("No undecrypted packets to process for bulk channel decrypt")
        return

    logger.info(
        "Starting bulk historical channel decryption of %d packets across %d channels",
        total,
        len(channels),
    )

    async for (
        packet_id,
        packet_data,
        packet_timestamp,
    ) in RawPacketRepository.stream_all_undecrypted():
        packet_info = parse_packet(packet_data)
        path_hex = packet_info.path.hex() if packet_info else None
        path_len = packet_info.path_length if packet_info else None

        for channel_key_bytes, channel_key_hex, channel_name in channels:
            result = try_decrypt_packet_with_channel_key(packet_data, channel_key_bytes)
            if result is None:
                continue

            msg_id = await create_message_from_decrypted(
                packet_id=packet_id,
                channel_key=channel_key_hex,
                channel_name=channel_name,
                sender=result.sender,
                message_text=result.message,
                timestamp=result.timestamp,
                received_at=packet_timestamp,
                path=path_hex,
                path_len=path_len,
                realtime=False,
            )
            if msg_id is not None:
                decrypted_count += 1
                matched_channel_names.add(channel_name)
            break

    logger.info(
        "Bulk historical channel decryption complete: %d/%d packets decrypted across %d channels",
        decrypted_count,
        total,
        len(matched_channel_names),
    )

    if decrypted_count > 0:
        broadcast_success(
            "Bulk historical decrypt complete",
            (
                f"Decrypted {decrypted_count} message{'s' if decrypted_count != 1 else ''} "
                f"across {len(matched_channel_names)} room"
                f"{'s' if len(matched_channel_names) != 1 else ''}"
            ),
        )


@router.get("", response_model=list[Channel])
async def list_channels() -> list[Channel]:
    """List all channels from the database."""
    return await ChannelRepository.get_all()


@router.get("/{key}/detail", response_model=ChannelDetail)
async def get_channel_detail(key: str) -> ChannelDetail:
    """Get comprehensive channel profile data with message statistics."""
    channel = await ChannelRepository.get_by_key(key)
    if not channel:
        raise HTTPException(status_code=404, detail="Channel not found")

    stats = await MessageRepository.get_channel_stats(channel.key)

    return ChannelDetail(
        channel=channel,
        message_counts=ChannelMessageCounts(**stats["message_counts"]),
        first_message_at=stats["first_message_at"],
        unique_sender_count=stats["unique_sender_count"],
        top_senders_24h=[ChannelTopSender(**s) for s in stats["top_senders_24h"]],
        path_hash_width_24h=stats["path_hash_width_24h"],
    )


@router.post("", response_model=Channel)
async def create_channel(request: CreateChannelRequest) -> Channel:
    """Create a channel in the database.

    Channels are NOT pushed to radio on creation. They are loaded to the radio
    automatically when sending a message (see messages.py send_channel_message).
    """
    requested_name = request.name
    key_hex, channel_name, is_hashtag = _derive_channel_identity(requested_name, request.key)

    logger.info("Creating channel %s: %s (hashtag=%s)", key_hex, channel_name, is_hashtag)

    # Store in database only - radio sync happens at send time
    await ChannelRepository.upsert(
        key=key_hex,
        name=channel_name,
        is_hashtag=is_hashtag,
        on_radio=False,
    )

    stored = await ChannelRepository.get_by_key(key_hex)
    if stored is None:
        raise HTTPException(status_code=500, detail="Channel was created but could not be reloaded")

    _broadcast_channel_update(stored)
    return stored


@router.post("/bulk-hashtag", response_model=BulkCreateHashtagChannelsResponse)
async def bulk_create_hashtag_channels(
    request: BulkCreateHashtagChannelsRequest,
    background_tasks: BackgroundTasks,
    response: Response,
) -> BulkCreateHashtagChannelsResponse:
    created_channels: list[Channel] = []
    existing_count = 0
    invalid_names: list[str] = []
    decrypt_started = False
    decrypt_total_packets = 0
    decrypt_targets: list[tuple[bytes, str, str]] = []

    for raw_name in request.channel_names:
        normalized_name = _normalize_bulk_hashtag_name(raw_name)
        if normalized_name is None:
            invalid_names.append(raw_name)
            continue

        key_hex, channel_name, is_hashtag = _derive_channel_identity(normalized_name)
        existing = await ChannelRepository.get_by_key(key_hex)
        if existing is not None:
            existing_count += 1
            continue

        await ChannelRepository.upsert(
            key=key_hex,
            name=channel_name,
            is_hashtag=is_hashtag,
            on_radio=False,
        )
        stored = await ChannelRepository.get_by_key(key_hex)
        if stored is None:
            raise HTTPException(
                status_code=500,
                detail="Channel was created but could not be reloaded",
            )

        created_channels.append(stored)
        decrypt_targets.append((bytes.fromhex(stored.key), stored.key, stored.name))
        _broadcast_channel_update(stored)

    if request.try_historical and decrypt_targets:
        decrypt_total_packets = await RawPacketRepository.get_undecrypted_count()
        if decrypt_total_packets > 0:
            background_tasks.add_task(
                _run_historical_channel_decryption_for_channels, decrypt_targets
            )
            decrypt_started = True
            response.status_code = status.HTTP_202_ACCEPTED

    message = (
        f"Created {len(created_channels)} room{'s' if len(created_channels) != 1 else ''}"
        if created_channels
        else "No new rooms were added"
    )
    if request.try_historical and decrypt_targets:
        if decrypt_started:
            message += (
                f" and started background decrypt of {decrypt_total_packets} packet"
                f"{'s' if decrypt_total_packets != 1 else ''}"
            )
        else:
            message += "; no undecrypted packets were available"

    return BulkCreateHashtagChannelsResponse(
        created_channels=created_channels,
        existing_count=existing_count,
        invalid_names=invalid_names,
        decrypt_started=decrypt_started,
        decrypt_total_packets=decrypt_total_packets,
        message=message,
    )


@router.post("/{key}/mark-read")
async def mark_channel_read(key: str) -> dict:
    """Mark a channel as read (update last_read_at timestamp)."""
    channel = await ChannelRepository.get_by_key(key)
    if not channel:
        raise HTTPException(status_code=404, detail="Channel not found")

    updated = await ChannelRepository.update_last_read_at(key)
    if not updated:
        raise HTTPException(status_code=500, detail="Failed to update read state")

    return {"status": "ok", "key": channel.key}


@router.post("/{key}/flood-scope-override", response_model=Channel)
async def set_channel_flood_scope_override(
    key: str, request: ChannelFloodScopeOverrideRequest
) -> Channel:
    """Set or clear a per-channel flood-scope override."""
    channel = await ChannelRepository.get_by_key(key)
    if not channel:
        raise HTTPException(status_code=404, detail="Channel not found")

    override = normalize_region_scope(request.flood_scope_override) or None
    updated = await ChannelRepository.update_flood_scope_override(channel.key, override)
    if not updated:
        raise HTTPException(status_code=500, detail="Failed to update flood-scope override")

    refreshed = await ChannelRepository.get_by_key(channel.key)
    if refreshed is None:
        raise HTTPException(status_code=500, detail="Channel disappeared after update")

    broadcast_event("channel", refreshed.model_dump())
    return refreshed


@router.post("/{key}/path-hash-mode-override", response_model=Channel)
async def set_channel_path_hash_mode_override(
    key: str, request: ChannelPathHashModeOverrideRequest
) -> Channel:
    """Set or clear a per-channel path hash mode override."""
    channel = await ChannelRepository.get_by_key(key)
    if not channel:
        raise HTTPException(status_code=404, detail="Channel not found")

    updated = await ChannelRepository.update_path_hash_mode_override(
        channel.key, request.path_hash_mode_override
    )
    if not updated:
        raise HTTPException(status_code=500, detail="Failed to update path-hash-mode override")

    refreshed = await ChannelRepository.get_by_key(channel.key)
    if refreshed is None:
        raise HTTPException(status_code=500, detail="Channel disappeared after update")

    broadcast_event("channel", refreshed.model_dump())
    return refreshed


@router.delete("/{key}")
async def delete_channel(key: str) -> dict:
    """Delete a channel from the database by key.

    Note: This does not clear the channel from the radio. The radio's channel
    slots are managed separately (channels are loaded temporarily when sending).
    """
    if is_public_channel_key(key):
        raise HTTPException(
            status_code=400, detail="The canonical Public channel cannot be deleted"
        )

    logger.info("Deleting channel %s from database", key)
    await ChannelRepository.delete(key)

    broadcast_event("channel_deleted", {"key": key})

    return {"status": "ok"}
