from __future__ import annotations

from asgiref.sync import sync_to_async

from core.commands.base import CommandContext, CommandResult
from core.commands.handlers.bp import (
    BPCommandHandler,
    bp_reply_is_optional_for_trigger,
    bp_subcommands_enabled,
    bp_trigger_matches,
)
from core.commands.policies import ensure_variant_policies_for_profile
from core.commands.registry import get as get_handler
from core.commands.registry import register
from core.messaging.reply_sync import is_mirrored_origin
from core.models import (
    CommandAction,
    CommandChannelBinding,
    CommandProfile,
    Message,
    PersonIdentifier,
)
from core.security.command_policy import CommandSecurityContext, evaluate_command_policy
from core.tasks.chat_defaults import ensure_default_source_for_chat
from core.util import logs

log = logs.get_logger("command_engine")

_REGISTERED = False
_SUPPORTED_PROFILE_SLUGS = {"bp"}


def _channel_variants(service: str, channel_identifier: str) -> list[str]:
    value = str(channel_identifier or "").strip()
    if not value:
        return []
    variants = [value]
    svc = str(service or "").strip().lower()
    if svc == "whatsapp":
        bare = value.split("@", 1)[0].strip()
        if bare and bare not in variants:
            variants.append(bare)
        group = f"{bare}@g.us" if bare else ""
        if group and group not in variants:
            variants.append(group)
    return variants


def _canonical_channel_identifier(service: str, channel_identifier: str) -> str:
    value = str(channel_identifier or "").strip()
    if not value:
        return ""
    if str(service or "").strip().lower() == "whatsapp":
        return value.split("@", 1)[0].strip()
    return value


def _signal_identifier_rank(identifier_value: str) -> int:
    identifier_text = str(identifier_value or "").strip()
    if not identifier_text:
        return 99
    if identifier_text.startswith("group."):
        return 0
    if identifier_text.startswith("+"):
        return 1
    return 2


def _expand_service_channel_variants(
    user_id: int,
    service: str,
    identifiers: list[str],
) -> list[str]:
    variants: list[str] = []
    for identifier in identifiers:
        for value in _channel_variants(service, identifier):
            if value and value not in variants:
                variants.append(value)
    if str(service or "").strip().lower() != "signal" or not variants:
        return variants
    person_ids = list(
        PersonIdentifier.objects.filter(
            user_id=user_id,
            service="signal",
            identifier__in=variants,
        )
        .values_list("person_id", flat=True)
        .distinct()
    )
    if not person_ids:
        return variants
    alias_rows = list(
        PersonIdentifier.objects.filter(
            user_id=user_id,
            service="signal",
            person_id__in=person_ids,
        ).values_list("identifier", flat=True)
    )
    for value in alias_rows:
        cleaned = str(value or "").strip()
        if cleaned and cleaned not in variants:
            variants.append(cleaned)
    variants.sort(key=lambda value: (_signal_identifier_rank(value), value))
    return variants


def _preferred_channel_identifier(service: str, identifiers: list[str]) -> str:
    cleaned = [str(value or "").strip() for value in identifiers if str(value or "").strip()]
    if not cleaned:
        return ""
    if str(service or "").strip().lower() == "signal":
        cleaned.sort(key=lambda value: (_signal_identifier_rank(value), value))
        return cleaned[0]
    return cleaned[0]


def _effective_bootstrap_scope(
    ctx: CommandContext,
    trigger_message: Message,
) -> tuple[str, str]:
    service = str(ctx.service or "").strip().lower()
    identifier = str(ctx.channel_identifier or "").strip()
    if service != "web":
        return service, identifier
    session_identifier = getattr(
        getattr(trigger_message, "session", None), "identifier", None
    )
    fallback_service = (
        str(getattr(session_identifier, "service", "") or "").strip().lower()
    )
    fallback_identifier = str(
        getattr(session_identifier, "identifier", "") or ""
    ).strip()
    if fallback_service and fallback_identifier and fallback_service != "web":
        return fallback_service, fallback_identifier
    return service, identifier


def _ensure_bp_profile(user_id: int) -> CommandProfile:
    profile, _ = CommandProfile.objects.get_or_create(
        user_id=user_id,
        slug="bp",
        defaults={
            "name": "Business Plan",
            "enabled": True,
            "trigger_token": ".bp",
            "reply_required": True,
            "exact_match_only": True,
            "window_scope": "conversation",
            "visibility_mode": "status_in_source",
        },
    )
    updated = False
    if not profile.enabled:
        profile.enabled = True
        updated = True
    if updated:
        profile.save(update_fields=["enabled", "updated_at"])
    if str(profile.trigger_token or "").strip() != ".bp":
        profile.trigger_token = ".bp"
        profile.save(update_fields=["trigger_token", "updated_at"])
    for action_type, position in (
        ("extract_bp", 0),
        ("save_document", 1),
        ("post_result", 2),
    ):
        action, created = CommandAction.objects.get_or_create(
            profile=profile,
            action_type=action_type,
            defaults={"enabled": True, "position": position},
        )
        if (not created) and (not action.enabled):
            action.enabled = True
            action.save(update_fields=["enabled", "updated_at"])
    ensure_variant_policies_for_profile(profile)
    return profile


def _ensure_profile_for_slug(user_id: int, slug: str) -> CommandProfile | None:
    if slug == "bp":
        return _ensure_bp_profile(user_id)
    return None


def _detected_bootstrap_slugs(message_text: str) -> list[str]:
    slugs: list[str] = []
    if bp_trigger_matches(message_text, ".bp", False):
        slugs.append("bp")
    return slugs


def _auto_setup_profile_bindings_for_first_command(
    ctx: CommandContext,
    trigger_message: Message,
) -> None:
    author = str(getattr(trigger_message, "custom_author", "") or "").strip().upper()
    if author != "USER":
        return
    slugs = _detected_bootstrap_slugs(ctx.message_text)
    if not slugs:
        return
    service, identifier = _effective_bootstrap_scope(ctx, trigger_message)
    service = str(service or "").strip().lower()
    canonical = _canonical_channel_identifier(service, identifier)
    variants = _expand_service_channel_variants(ctx.user_id, service, [canonical])
    canonical = _preferred_channel_identifier(service, variants) or canonical
    if not service or not variants:
        return
    for slug in slugs:
        profile = _ensure_profile_for_slug(ctx.user_id, slug)
        if profile is None:
            continue
        already_enabled = CommandChannelBinding.objects.filter(
            profile=profile,
            enabled=True,
            direction="ingress",
            service=service,
            channel_identifier__in=variants,
        ).exists()
        if already_enabled:
            continue
        for direction in ("ingress", "egress"):
            binding, _ = CommandChannelBinding.objects.get_or_create(
                profile=profile,
                direction=direction,
                service=service,
                channel_identifier=canonical,
                defaults={"enabled": True},
            )
            if not binding.enabled:
                binding.enabled = True
                binding.save(update_fields=["enabled", "updated_at"])
            alternate_variants = [value for value in variants if value != canonical]
            if alternate_variants:
                CommandChannelBinding.objects.filter(
                    profile=profile,
                    direction=direction,
                    service=service,
                    channel_identifier__in=alternate_variants,
                ).update(enabled=False)
    ensure_default_source_for_chat(
        user=trigger_message.user,
        service=service,
        channel_identifier=canonical,
        message=trigger_message,
    )


def ensure_handlers_registered():
    global _REGISTERED
    if _REGISTERED:
        return
    register(BPCommandHandler())
    _REGISTERED = True


async def _eligible_profiles(ctx: CommandContext) -> list[CommandProfile]:
    def _load():
        trigger = (
            Message.objects.select_related("session", "session__identifier")
            .filter(id=ctx.message_id, user_id=ctx.user_id)
            .first()
        )
        direct_variants = _expand_service_channel_variants(
            ctx.user_id,
            ctx.service,
            [ctx.channel_identifier],
        )
        source_channel = str(getattr(trigger, "source_chat_id", "") or "").strip()
        for expanded in _expand_service_channel_variants(
            ctx.user_id,
            ctx.service,
            [source_channel],
        ):
            if expanded and expanded not in direct_variants:
                direct_variants.append(expanded)
        if not direct_variants:
            return []
        direct = list(
            CommandProfile.objects.filter(
                user_id=ctx.user_id,
                enabled=True,
                slug__in=_SUPPORTED_PROFILE_SLUGS,
                channel_bindings__enabled=True,
                channel_bindings__direction="ingress",
                channel_bindings__service=ctx.service,
                channel_bindings__channel_identifier__in=direct_variants,
            ).distinct()
        )
        if direct:
            return direct
        # Compose-originated messages use `web` service even when the
        # underlying conversation is mapped to a platform identifier.
        if str(ctx.service or "").strip().lower() != "web":
            return []
        identifier = getattr(getattr(trigger, "session", None), "identifier", None)
        fallback_service = str(getattr(identifier, "service", "") or "").strip().lower()
        fallback_identifier = str(getattr(identifier, "identifier", "") or "").strip()
        fallback_variants = _expand_service_channel_variants(
            ctx.user_id,
            fallback_service,
            [fallback_identifier],
        )
        for expanded in _expand_service_channel_variants(
            ctx.user_id,
            fallback_service,
            [source_channel],
        ):
            if expanded and expanded not in fallback_variants:
                fallback_variants.append(expanded)
        if not fallback_service or not fallback_variants:
            return []
        return list(
            CommandProfile.objects.filter(
                user_id=ctx.user_id,
                enabled=True,
                slug__in=_SUPPORTED_PROFILE_SLUGS,
                channel_bindings__enabled=True,
                channel_bindings__direction="ingress",
                channel_bindings__service=fallback_service,
                channel_bindings__channel_identifier__in=fallback_variants,
            ).distinct()
        )

    return await sync_to_async(_load)()


def _matches_trigger(profile: CommandProfile, text: str) -> bool:
    if profile.slug == "bp" and bp_subcommands_enabled():
        return bp_trigger_matches(
            message_text=text,
            trigger_token=profile.trigger_token,
            exact_match_only=profile.exact_match_only,
        )
    body = str(text or "").strip()
    trigger = str(profile.trigger_token or "").strip()
    if not trigger:
        return False
    if profile.exact_match_only:
        return body == trigger
    return trigger in body


async def process_inbound_message(ctx: CommandContext) -> list[CommandResult]:
    ensure_handlers_registered()
    trigger_message = await sync_to_async(
        lambda: Message.objects.select_related("user", "session", "session__identifier")
        .filter(id=ctx.message_id)
        .first()
    )()
    if trigger_message is None:
        return []
    if is_mirrored_origin(trigger_message.message_meta):
        return []
    effective_service, effective_channel = _effective_bootstrap_scope(
        ctx, trigger_message
    )
    security_context = CommandSecurityContext(
        service=effective_service,
        channel_identifier=effective_channel,
        message_meta=dict(getattr(trigger_message, "message_meta", {}) or {}),
        payload=dict(ctx.payload or {}),
    )
    await sync_to_async(_auto_setup_profile_bindings_for_first_command)(
        ctx,
        trigger_message,
    )

    profiles = await _eligible_profiles(ctx)
    results: list[CommandResult] = []
    for profile in profiles:
        if not _matches_trigger(profile, ctx.message_text):
            continue
        decision = await sync_to_async(evaluate_command_policy)(
            user=trigger_message.user,
            scope_key=f"command.{profile.slug}",
            context=security_context,
        )
        if not decision.allowed:
            results.append(
                CommandResult(
                    ok=False,
                    status="skipped",
                    error=f"policy_denied:{decision.code}",
                    payload={
                        "profile": profile.slug,
                        "scope": f"command.{profile.slug}",
                        "reason": decision.reason,
                    },
                )
            )
            continue
        if profile.reply_required and trigger_message.reply_to_id is None:
            if (
                profile.slug == "bp"
                and bp_subcommands_enabled()
                and bp_reply_is_optional_for_trigger(ctx.message_text)
            ):
                pass
            else:
                results.append(
                    CommandResult(
                        ok=False,
                        status="skipped",
                        error="reply_required",
                        payload={"profile": profile.slug},
                    )
                )
                continue
        handler = get_handler(profile.slug)
        if handler is None:
            results.append(
                CommandResult(
                    ok=False,
                    status="failed",
                    error=f"missing_handler:{profile.slug}",
                )
            )
            continue
        try:
            result = await handler.execute(ctx)
            results.append(result)
        except Exception as exc:
            log.exception(
                "command execution failed for profile=%s: %s", profile.slug, exc
            )
            results.append(
                CommandResult(
                    ok=False,
                    status="failed",
                    error=f"handler_exception:{exc}",
                )
            )
    return results
