from __future__ import annotations

import re
import time

from asgiref.sync import sync_to_async
from django.conf import settings

from core.commands.base import CommandContext, CommandHandler, CommandResult
from core.commands.delivery import post_status_in_source, post_to_channel_binding
from core.commands.policies import BP_VARIANT_META, load_variant_policy
from core.messaging import ai as ai_runner
from core.messaging.text_export import plain_text_blob
from core.messaging.utils import messages_to_string
from core.models import (
    AI,
    BusinessPlanDocument,
    BusinessPlanRevision,
    CommandAction,
    CommandChannelBinding,
    CommandRun,
    CommandVariantPolicy,
    Message,
)

_BP_ROOT_RE = re.compile(r"^\s*(?:\.bp\b|#bp#?)\s*$", re.IGNORECASE)
_BP_SET_RE = re.compile(
    r"^\s*(?:\.bp\s+set\b|#bp\s+set#?)(?P<rest>.*)$",
    re.IGNORECASE | re.DOTALL,
)
_BP_SET_RANGE_RE = re.compile(
    r"^\s*(?:\.bp\s+set\s+range\b|#bp\s+set\s+range#?)(?:.*)$",
    re.IGNORECASE | re.DOTALL,
)


class BPParsedCommand(dict):
    @property
    def command(self) -> str | None:
        value = self.get("command")
        return str(value) if value else None

    @property
    def remainder_text(self) -> str:
        return str(self.get("remainder_text") or "")


def parse_bp_subcommand(text: str) -> BPParsedCommand:
    body = str(text or "")
    if _BP_SET_RANGE_RE.match(body):
        return BPParsedCommand(command="set_range", remainder_text="")
    match = _BP_SET_RE.match(body)
    if match:
        return BPParsedCommand(
            command="set", remainder_text=str(match.group("rest") or "").strip()
        )
    return BPParsedCommand(command=None, remainder_text="")


def bp_subcommands_enabled() -> bool:
    raw = getattr(settings, "BP_SUBCOMMANDS_V1", True)
    if raw is None:
        return True
    return bool(raw)


def bp_trigger_matches(
    message_text: str, trigger_token: str, exact_match_only: bool
) -> bool:
    body = str(message_text or "").strip()
    trigger = str(trigger_token or "").strip()
    parsed = parse_bp_subcommand(body)
    if parsed.command and bp_subcommands_enabled():
        return True
    if _BP_ROOT_RE.match(body):
        return True
    if not trigger:
        return False
    if exact_match_only:
        return body.lower() == trigger.lower()
    return trigger.lower() in body.lower()


def bp_reply_is_optional_for_trigger(message_text: str) -> bool:
    parsed = parse_bp_subcommand(message_text)
    return parsed.command == "set"


def _bp_system_prompt():
    return (
        "Create a structured business plan using the given template. "
        "Follow the template section order exactly. "
        "If data is missing, write concise assumptions and risks. "
        "Return markdown only."
    )


def _clamp_transcript(transcript: str, max_chars: int) -> str:
    text = str(transcript or "")
    if max_chars <= 0 or len(text) <= max_chars:
        return text
    head_size = min(2000, max_chars // 3)
    tail_size = max(0, max_chars - head_size - 140)
    omitted = len(text) - head_size - tail_size
    return (
        text[:head_size].rstrip()
        + f"\n\n[... truncated {max(0, omitted)} chars ...]\n\n"
        + text[-tail_size:].lstrip()
    )


class BPCommandHandler(CommandHandler):
    slug = "bp"

    def _variant_key_for_text(self, text: str) -> str:
        parsed = parse_bp_subcommand(text)
        if parsed.command == "set":
            return "bp_set"
        if parsed.command == "set_range":
            return "bp_set_range"
        return "bp"

    def _variant_display_name(self, variant_key: str) -> str:
        meta = BP_VARIANT_META.get(str(variant_key or "").strip(), {})
        return str(meta.get("name") or variant_key or "bp")

    async def _effective_policy(
        self,
        *,
        profile,
        variant_key: str,
        action_types: set[str],
    ) -> dict:
        policy = await sync_to_async(load_variant_policy)(profile, variant_key)
        if isinstance(policy, CommandVariantPolicy):
            return {
                "enabled": bool(policy.enabled),
                "generation_mode": str(policy.generation_mode or "verbatim"),
                "send_plan_to_egress": bool(policy.send_plan_to_egress)
                and ("post_result" in action_types),
                "send_status_to_source": bool(policy.send_status_to_source)
                or str(profile.visibility_mode or "") == "status_in_source",
                "send_status_to_egress": bool(policy.send_status_to_egress),
                "store_document": bool(getattr(policy, "store_document", True)),
            }
        return {
            "enabled": True,
            "generation_mode": "ai" if variant_key == "bp" else "verbatim",
            "send_plan_to_egress": "post_result" in action_types,
            "send_status_to_source": str(profile.visibility_mode or "")
            == "status_in_source",
            "send_status_to_egress": False,
            "store_document": True,
        }

    async def _fanout(self, run: CommandRun, text: str) -> dict:
        profile = run.profile
        trigger = await sync_to_async(
            lambda: Message.objects.select_related("session", "user")
            .filter(id=run.trigger_message_id)
            .first()
        )()
        if trigger is None:
            return {"sent_bindings": 0, "failed_bindings": 0}
        bindings = await sync_to_async(list)(
            CommandChannelBinding.objects.filter(
                profile=profile,
                enabled=True,
                direction="egress",
            )
        )
        sent_bindings = 0
        failed_bindings = 0
        for binding in bindings:
            ok = await post_to_channel_binding(
                trigger_message=trigger,
                binding_service=binding.service,
                binding_channel_identifier=binding.channel_identifier,
                text=text,
                origin_tag=f"bp:{run.id}",
                command_slug=self.slug,
            )
            if ok:
                sent_bindings += 1
            else:
                failed_bindings += 1
        return {"sent_bindings": sent_bindings, "failed_bindings": failed_bindings}

    async def _fanout_status(self, run: CommandRun, text: str) -> dict:
        profile = run.profile
        trigger = await sync_to_async(
            lambda: Message.objects.select_related("session", "user")
            .filter(id=run.trigger_message_id)
            .first()
        )()
        if trigger is None:
            return {"sent_bindings": 0, "failed_bindings": 0}
        bindings = await sync_to_async(list)(
            CommandChannelBinding.objects.filter(
                profile=profile,
                enabled=True,
                direction="egress",
            )
        )
        sent_bindings = 0
        failed_bindings = 0
        for binding in bindings:
            ok = await post_to_channel_binding(
                trigger_message=trigger,
                binding_service=binding.service,
                binding_channel_identifier=binding.channel_identifier,
                text=text,
                origin_tag=f"bp-status-egress:{run.id}",
                command_slug=self.slug,
            )
            if ok:
                sent_bindings += 1
            else:
                failed_bindings += 1
        return {"sent_bindings": sent_bindings, "failed_bindings": failed_bindings}

    async def _load_window(self, trigger: Message, anchor: Message) -> list[Message]:
        return await sync_to_async(list)(
            Message.objects.filter(
                user=trigger.user,
                session=trigger.session,
                ts__gte=int(anchor.ts or 0),
                ts__lte=int(trigger.ts or 0),
            )
            .order_by("ts")
            .select_related(
                "session", "session__identifier", "session__identifier__person"
            )
        )

    def _annotation(
        self, mode: str, message_count: int, has_addendum: bool = False
    ) -> str:
        if mode == "set" and has_addendum:
            return "Generated from 1 message + 1 addendum."
        if message_count == 1:
            return "Generated from 1 message."
        return f"Generated from {int(message_count)} messages."

    async def _persist_document(
        self,
        *,
        run: CommandRun,
        trigger: Message,
        profile,
        anchor: Message | None,
        content: str,
        mode: str,
        source_message_ids: list[str],
        annotation: str,
    ) -> BusinessPlanDocument:
        payload = {
            "mode": mode,
            "source_message_ids": list(source_message_ids),
            "annotation": annotation,
        }
        document = await sync_to_async(BusinessPlanDocument.objects.create)(
            user=trigger.user,
            command_profile=profile,
            source_service=trigger.source_service or "web",
            source_channel_identifier=trigger.source_chat_id or "",
            trigger_message=trigger,
            anchor_message=anchor,
            title=f"Business Plan {time.strftime('%Y-%m-%d %H:%M:%S')}",
            status="draft",
            content_markdown=content,
            structured_payload=payload,
        )
        await sync_to_async(BusinessPlanRevision.objects.create)(
            document=document,
            editor_user=trigger.user,
            content_markdown=content,
            structured_payload=payload,
        )
        run.result_ref = document
        await sync_to_async(run.save)(update_fields=["result_ref", "updated_at"])
        return document

    async def _execute_set_or_range(
        self,
        *,
        trigger: Message,
        run: CommandRun,
        profile,
        policy: dict,
        variant_key: str,
        parsed: BPParsedCommand,
    ) -> CommandResult:
        mode = str(parsed.command or "")
        remainder = parsed.remainder_text
        anchor = trigger.reply_to

        if mode == "set_range":
            if anchor is None:
                run.status = "failed"
                run.error = "bp_set_range_requires_reply_target"
                await sync_to_async(run.save)(
                    update_fields=["status", "error", "updated_at"]
                )
                return CommandResult(ok=False, status="failed", error=run.error)
            rows = await self._load_window(trigger, anchor)
            deterministic_content = plain_text_blob(rows)
            if not deterministic_content.strip():
                run.status = "failed"
                run.error = "bp_set_range_empty_content"
                await sync_to_async(run.save)(
                    update_fields=["status", "error", "updated_at"]
                )
                return CommandResult(ok=False, status="failed", error=run.error)
            if str(policy.get("generation_mode") or "verbatim") == "ai":
                ai_obj = await sync_to_async(
                    lambda: AI.objects.filter(user=trigger.user).first()
                )()
                if ai_obj is None:
                    run.status = "failed"
                    run.error = "ai_not_configured"
                    await sync_to_async(run.save)(
                        update_fields=["status", "error", "updated_at"]
                    )
                    return CommandResult(ok=False, status="failed", error=run.error)
                prompt = [
                    {
                        "role": "system",
                        "content": (
                            "Transform source chat text into a structured business plan in markdown. "
                            "Do not reference any user template."
                        ),
                    },
                    {"role": "user", "content": deterministic_content},
                ]
                try:
                    content = str(
                        await ai_runner.run_prompt(
                            prompt,
                            ai_obj,
                            operation="command_bp_set_range_extract",
                        )
                        or ""
                    ).strip()
                except Exception as exc:
                    run.status = "failed"
                    run.error = f"bp_ai_failed:{exc}"
                    await sync_to_async(run.save)(
                        update_fields=["status", "error", "updated_at"]
                    )
                    return CommandResult(ok=False, status="failed", error=run.error)
                if not content:
                    run.status = "failed"
                    run.error = "empty_ai_response"
                    await sync_to_async(run.save)(
                        update_fields=["status", "error", "updated_at"]
                    )
                    return CommandResult(ok=False, status="failed", error=run.error)
            else:
                content = deterministic_content
            annotation = self._annotation("set_range", len(rows))
            doc = None
            if bool(policy.get("store_document", True)):
                doc = await self._persist_document(
                    run=run,
                    trigger=trigger,
                    profile=profile,
                    anchor=anchor,
                    content=content,
                    mode="set_range",
                    source_message_ids=[str(row.id) for row in rows],
                    annotation=annotation,
                )
        elif mode == "set":
            source_ids: list[str] = []
            if anchor is not None and not remainder:
                content = str(anchor.text or "").strip() or "(no text)"
                source_ids.append(str(anchor.id))
                has_addendum = False
            elif anchor is not None and remainder:
                base = str(anchor.text or "").strip() or "(no text)"
                content = (
                    f"{base}\n" "--- Addendum (newer message text) ---\n" f"{remainder}"
                )
                source_ids.extend([str(anchor.id), str(trigger.id)])
                has_addendum = True
            elif remainder:
                content = remainder
                source_ids.append(str(trigger.id))
                has_addendum = False
            else:
                run.status = "failed"
                run.error = "bp_set_empty_content"
                await sync_to_async(run.save)(
                    update_fields=["status", "error", "updated_at"]
                )
                return CommandResult(ok=False, status="failed", error=run.error)

            if str(policy.get("generation_mode") or "verbatim") == "ai":
                ai_obj = await sync_to_async(
                    lambda: AI.objects.filter(user=trigger.user).first()
                )()
                if ai_obj is None:
                    run.status = "failed"
                    run.error = "ai_not_configured"
                    await sync_to_async(run.save)(
                        update_fields=["status", "error", "updated_at"]
                    )
                    return CommandResult(ok=False, status="failed", error=run.error)
                prompt = [
                    {
                        "role": "system",
                        "content": (
                            "Transform source chat text into a structured business plan in markdown. "
                            "Do not reference any user template."
                        ),
                    },
                    {"role": "user", "content": content},
                ]
                try:
                    ai_content = str(
                        await ai_runner.run_prompt(
                            prompt,
                            ai_obj,
                            operation="command_bp_set_extract",
                        )
                        or ""
                    ).strip()
                except Exception as exc:
                    run.status = "failed"
                    run.error = f"bp_ai_failed:{exc}"
                    await sync_to_async(run.save)(
                        update_fields=["status", "error", "updated_at"]
                    )
                    return CommandResult(ok=False, status="failed", error=run.error)
                if not ai_content:
                    run.status = "failed"
                    run.error = "empty_ai_response"
                    await sync_to_async(run.save)(
                        update_fields=["status", "error", "updated_at"]
                    )
                    return CommandResult(ok=False, status="failed", error=run.error)
                content = ai_content

            annotation = self._annotation(
                "set", 1 if not has_addendum else 2, has_addendum
            )
            doc = None
            if bool(policy.get("store_document", True)):
                doc = await self._persist_document(
                    run=run,
                    trigger=trigger,
                    profile=profile,
                    anchor=anchor,
                    content=content,
                    mode="set",
                    source_message_ids=source_ids,
                    annotation=annotation,
                )
        else:
            run.status = "failed"
            run.error = "bp_unknown_subcommand"
            await sync_to_async(run.save)(
                update_fields=["status", "error", "updated_at"]
            )
            return CommandResult(ok=False, status="failed", error=run.error)

        fanout_stats = {"sent_bindings": 0, "failed_bindings": 0}
        if bool(policy.get("send_plan_to_egress")):
            fanout_body = f"{content}\n\n{annotation}".strip()
            fanout_stats = await self._fanout(run, fanout_body)

        sent_count = int(fanout_stats.get("sent_bindings") or 0)
        failed_count = int(fanout_stats.get("failed_bindings") or 0)
        status_text = (
            f"[bp:{self._variant_display_name(variant_key)}:{policy.get('generation_mode')}] "
            f"{annotation.strip()} "
            f"{'Saved as ' + doc.title + ' · ' if doc else 'Not saved (store_document disabled) · '}"
            f"fanout sent:{sent_count}"
        ).strip()
        if failed_count:
            status_text += f" failed:{failed_count}"

        if bool(policy.get("send_status_to_source")):
            await post_status_in_source(
                trigger_message=trigger,
                text=status_text,
                origin_tag=f"bp-status:{trigger.id}",
            )
        if bool(policy.get("send_status_to_egress")):
            await self._fanout_status(run, status_text)

        run.status = "ok"
        run.error = ""
        await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
        return CommandResult(
            ok=True,
            status="ok",
            payload={"document_id": str(doc.id) if doc else ""},
        )

    async def _execute_legacy_ai(
        self,
        *,
        trigger: Message,
        run: CommandRun,
        profile,
        policy: dict,
        variant_key: str,
    ) -> CommandResult:
        if trigger.reply_to_id is None:
            run.status = "failed"
            run.error = "bp_requires_reply_target"
            await sync_to_async(run.save)(
                update_fields=["status", "error", "updated_at"]
            )
            return CommandResult(ok=False, status="failed", error=run.error)

        anchor = trigger.reply_to
        rows = await self._load_window(trigger, anchor)
        transcript = messages_to_string(
            rows,
            author_rewrites={"USER": "Operator", "BOT": "Assistant"},
        )
        max_transcript_chars = int(
            getattr(settings, "BP_MAX_TRANSCRIPT_CHARS", 12000) or 12000
        )
        transcript = _clamp_transcript(transcript, max_transcript_chars)
        default_template = (
            "Business Plan:\n"
            "- Objective\n"
            "- Audience\n"
            "- Offer\n"
            "- GTM\n"
            "- Risks"
        )
        template_text = profile.template_text or default_template
        max_template_chars = int(
            getattr(settings, "BP_MAX_TEMPLATE_CHARS", 5000) or 5000
        )
        template_text = str(template_text or "")[:max_template_chars]
        generation_mode = str(policy.get("generation_mode") or "ai")
        if generation_mode == "verbatim":
            summary = plain_text_blob(rows)
            if not summary.strip():
                run.status = "failed"
                run.error = "bp_verbatim_empty_content"
                await sync_to_async(run.save)(
                    update_fields=["status", "error", "updated_at"]
                )
                return CommandResult(ok=False, status="failed", error=run.error)
        else:
            ai_obj = await sync_to_async(
                lambda: AI.objects.filter(user=trigger.user).first()
            )()
            if ai_obj is None:
                run.status = "failed"
                run.error = "ai_not_configured"
                await sync_to_async(run.save)(
                    update_fields=["status", "error", "updated_at"]
                )
                return CommandResult(ok=False, status="failed", error=run.error)

            prompt = [
                {"role": "system", "content": _bp_system_prompt()},
                {
                    "role": "user",
                    "content": (
                        "Template:\n"
                        f"{template_text}\n\n"
                        "Messages:\n"
                        f"{transcript}"
                    ),
                },
            ]
            try:
                summary = str(
                    await ai_runner.run_prompt(
                        prompt, ai_obj, operation="command_bp_extract"
                    )
                    or ""
                ).strip()
                if not summary:
                    raise RuntimeError("empty_ai_response")
            except Exception as exc:
                run.status = "failed"
                run.error = f"bp_ai_failed:{exc}"
                await sync_to_async(run.save)(
                    update_fields=["status", "error", "updated_at"]
                )
                return CommandResult(ok=False, status="failed", error=run.error)

        annotation = self._annotation("legacy", len(rows))
        document = None
        if bool(policy.get("store_document", True)):
            document = await self._persist_document(
                run=run,
                trigger=trigger,
                profile=profile,
                anchor=anchor,
                content=summary,
                mode="legacy_ai",
                source_message_ids=[str(row.id) for row in rows],
                annotation=annotation,
            )

        fanout_stats = {"sent_bindings": 0, "failed_bindings": 0}
        if bool(policy.get("send_plan_to_egress")):
            fanout_stats = await self._fanout(run, summary)

        sent_count = int(fanout_stats.get("sent_bindings") or 0)
        failed_count = int(fanout_stats.get("failed_bindings") or 0)
        status_text = (
            f"[bp:{self._variant_display_name(variant_key)}:{generation_mode}] "
            f"Generated business plan: "
            f"{document.title if document else 'not saved (store_document disabled)'} "
            f"· fanout sent:{sent_count}"
        )
        if failed_count:
            status_text += f" failed:{failed_count}"

        if bool(policy.get("send_status_to_source")):
            await post_status_in_source(
                trigger_message=trigger,
                text=status_text,
                origin_tag=f"bp-status:{trigger.id}",
            )
        if bool(policy.get("send_status_to_egress")):
            await self._fanout_status(run, status_text)

        run.status = "ok"
        run.error = ""
        await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
        return CommandResult(
            ok=True,
            status="ok",
            payload={"document_id": str(document.id) if document else ""},
        )

    async def execute(self, ctx: CommandContext) -> CommandResult:
        trigger = await sync_to_async(
            lambda: Message.objects.select_related("user", "session")
            .filter(id=ctx.message_id)
            .first()
        )()
        if trigger is None:
            return CommandResult(ok=False, status="failed", error="trigger_not_found")

        profile = await sync_to_async(
            lambda: trigger.user.commandprofile_set.filter(
                slug=self.slug, enabled=True
            ).first()
        )()
        if profile is None:
            return CommandResult(ok=False, status="skipped", error="profile_missing")

        actions = await sync_to_async(list)(
            CommandAction.objects.filter(profile=profile, enabled=True).order_by(
                "position", "id"
            )
        )
        action_types = {row.action_type for row in actions}
        if "extract_bp" not in action_types:
            return CommandResult(
                ok=False, status="skipped", error="extract_bp_disabled"
            )

        run, created = await sync_to_async(CommandRun.objects.get_or_create)(
            profile=profile,
            trigger_message=trigger,
            defaults={"user": trigger.user, "status": "running"},
        )
        if not created and run.status in {"ok", "running"}:
            return CommandResult(
                ok=True,
                status="ok",
                payload={"document_id": str(run.result_ref_id or "")},
            )

        run.status = "running"
        run.error = ""
        await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])

        variant_key = self._variant_key_for_text(ctx.message_text)
        policy = await self._effective_policy(
            profile=profile,
            variant_key=variant_key,
            action_types=action_types,
        )
        if not bool(policy.get("enabled")):
            run.status = "skipped"
            run.error = f"variant_disabled:{variant_key}"
            await sync_to_async(run.save)(
                update_fields=["status", "error", "updated_at"]
            )
            return CommandResult(ok=False, status="skipped", error=run.error)

        parsed = parse_bp_subcommand(ctx.message_text)
        if parsed.command and bp_subcommands_enabled():
            return await self._execute_set_or_range(
                trigger=trigger,
                run=run,
                profile=profile,
                policy=policy,
                variant_key=variant_key,
                parsed=parsed,
            )

        return await self._execute_legacy_ai(
            trigger=trigger,
            run=run,
            profile=profile,
            policy=policy,
            variant_key=variant_key,
        )
