from collections.abc import Iterable
from dataclasses import dataclass
import logging

from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import transaction
from django.http import HttpRequest
from django.contrib import messages
from django.urls import reverse
from django.utils import timezone
from django.utils.html import format_html

from agents.services import PretrainedWorkerTemplateService
from api.agent.comms.message_service import _ensure_participant, _get_or_create_conversation, _save_attachments
from api.agent.files.filespace_service import import_message_attachments_to_filespace
from api.agent.tasks import process_agent_events_task
from api.agent.tools.custom_tools import CUSTOM_TOOL_PREFIX
from api.agent.tools.tool_manager import mark_tool_enabled_without_discovery
from api.agent.core.llm_config import AgentLLMTier, resolve_intelligence_tier_for_owner
from api.agent.tools.mcp_manager import get_mcp_manager
from api.models import (
    CommsChannel,
    IntelligenceTier,
    MCPServerConfig,
    Organization,
    PersistentAgent,
    PersistentAgentCommsEndpoint,
    PersistentAgentConversationParticipant,
    PersistentAgentMessage,
    PersistentAgentSmsEndpoint,
    build_web_agent_address,
    build_web_user_address,
)
from api.services.persistent_agents import (
    PersistentAgentProvisioningError,
    PersistentAgentProvisioningService,
    ensure_default_agent_email_endpoint,
)
from api.services.owner_execution_pause import get_owner_account_pause_state
from api.services.signup_preview import get_signup_preview_creation_state
from api.services.signup_preview import user_has_existing_personal_agent_for_signup_preview
from api.pipedream_app_utils import normalize_app_slugs
from api.services.pipedream_apps import get_owner_selected_app_slugs, set_owner_selected_app_slugs
from console.context_helpers import build_console_context
from marketing_events.custom_events import ConfiguredCustomEvent, emit_configured_custom_capi_event
from util import sms
from util.analytics import Analytics, AnalyticsEvent, AnalyticsSource
from util.onboarding import clear_trial_onboarding_intent
from util.personal_signup_preview import (
    resolve_personal_signup_preview,
    SIGNUP_PREVIEW_EXISTING_AGENT_MESSAGE,
)
from util.sms import find_unused_number, get_user_primary_sms_number
from util.subscription_helper import get_owner_plan
from util.trial_enforcement import (
    PERSONAL_USAGE_REQUIRES_TRIAL_MESSAGE,
    TrialRequiredValidationError,
    can_user_use_personal_agents_and_api,
)

logger = logging.getLogger(__name__)
AGENT_SELECTED_PIPEDREAM_APP_SLUGS_SESSION_KEY = "agent_selected_pipedream_app_slugs"


def _customer_account_pause_creation_message(owner) -> str:
    state = get_owner_account_pause_state(owner)
    resume_at = state.get("resume_at")
    if resume_at is not None:
        local_resume_at = timezone.localtime(resume_at)
        return (
            "Your account is paused until "
            f"{local_resume_at.strftime('%b %d, %Y at %I:%M %p %Z')}. "
            "New agent creation is disabled until billing resumes."
        )
    return "Your account is paused. New agent creation is disabled until billing resumes."


def _apply_pending_pipedream_app_selections(
    request: HttpRequest,
    *,
    organization: Organization | None,
    selected_pipedream_app_slugs: Iterable[object] | None = None,
) -> None:
    pending_source = (
        request.session.get(AGENT_SELECTED_PIPEDREAM_APP_SLUGS_SESSION_KEY) or []
        if selected_pipedream_app_slugs is None
        else selected_pipedream_app_slugs
    )
    pending_slugs = normalize_app_slugs(pending_source)
    if not pending_slugs:
        return

    owner_scope = (
        MCPServerConfig.Scope.ORGANIZATION
        if organization is not None
        else MCPServerConfig.Scope.USER
    )
    owner_user = None if organization is not None else request.user
    owner_org = organization
    existing_slugs = get_owner_selected_app_slugs(
        owner_scope,
        owner_user=owner_user,
        owner_org=owner_org,
    )
    merged_slugs = normalize_app_slugs([*existing_slugs, *pending_slugs])
    selected_slugs = set_owner_selected_app_slugs(
        owner_scope,
        merged_slugs,
        owner_user=owner_user,
        owner_org=owner_org,
    )
    owner_id = str(organization.id) if organization is not None else str(request.user.id)

    def _refresh_owner_cache() -> None:
        manager = get_mcp_manager()
        manager.invalidate_pipedream_owner_cache(owner_scope, owner_id)
        manager.prewarm_pipedream_owner_cache(owner_scope, owner_id, app_slugs=selected_slugs)

    transaction.on_commit(_refresh_owner_cache)


@dataclass(frozen=True)
class AgentCreationResult:
    agent: PersistentAgent
    organization: Organization | None
    applied_schedule: str | None
    template_code: str | None
    contact_email: str | None
    contact_sms: str | None
    preferred_contact_method: str
    initial_message: str


def create_persistent_agent_from_charter(
    request: HttpRequest,
    *,
    initial_message: str,
    initial_attachments: Iterable[object] | None = None,
    contact_email: str | None,
    email_enabled: bool,
    sms_enabled: bool,
    preferred_contact_method: str,
    web_enabled: bool = False,
    preferred_llm_tier_key: str | None = None,
    charter_override: str | None = None,
    selected_pipedream_app_slugs: Iterable[object] | None = None,
) -> AgentCreationResult:
    initial_message = (initial_message or "").strip()
    if not initial_message:
        raise ValidationError("Please start by describing what your agent should do.")
    initial_attachments_list = list(initial_attachments or [])
    charter_text = (charter_override or initial_message).strip()

    preferred_contact_method = (preferred_contact_method or "email").strip().lower()
    contact_email = (contact_email or "").strip()

    template_code = request.session.get(PretrainedWorkerTemplateService.TEMPLATE_SESSION_KEY)
    selected_template = PretrainedWorkerTemplateService.get_template_by_code(template_code) if template_code else None

    resolved_context = build_console_context(request)
    preview_config = resolve_personal_signup_preview(
        request.user,
        request=request,
        current_context_type=resolved_context.current_context.type,
    )
    organization = None
    if resolved_context.current_context.type == "organization":
        membership = resolved_context.current_membership
        if membership is None:
            messages.error(
                request,
                "You no longer have access to that organization. Creating a personal agent instead.",
            )
        elif not resolved_context.can_manage_org_agents:
            raise ValidationError(
                "You need to be an organization owner or admin to create agents for this organization."
            )
        else:
            organization = membership.org

    owner = organization or request.user
    if get_owner_account_pause_state(owner).get("customer_paused"):
        raise ValidationError(_customer_account_pause_creation_message(owner))

    if organization is not None:
        billing = getattr(organization, "billing", None)
        seats_purchased = getattr(billing, "purchased_seats", 0) if billing else 0
        if seats_purchased <= 0:
            billing_url = f"{reverse('billing')}?org_id={organization.id}"
            request.session["context_type"] = "organization"
            request.session["context_id"] = str(organization.id)
            request.session["context_name"] = organization.name
            request.session.modified = True

            message_text = format_html(
                "Looks like your organization doesn't have any seats yet. "
                "<a class=\"underline font-medium\" href=\"{}\">Add seats in Billing</a> "
                "to create organization-owned agents.",
                billing_url,
            )
            raise ValidationError(message_text)

    if organization is None and not can_user_use_personal_agents_and_api(request.user):
        preview_creation_allowed = preview_config.processing_limit_enabled
        if not preview_creation_allowed:
            raise TrialRequiredValidationError(PERSONAL_USAGE_REQUIRES_TRIAL_MESSAGE)
    else:
        preview_creation_allowed = False

    if email_enabled and not contact_email:
        raise ValidationError("Please provide an email address for agent contact.")

    user_contact_email = contact_email if email_enabled else None
    user_contact_sms = None
    sms_preferred = preferred_contact_method == "sms"
    web_preferred = preferred_contact_method == "web"
    web_enabled = bool(web_enabled or web_preferred)

    preferred_llm_tier = None
    if preferred_llm_tier_key:
        tier_key = preferred_llm_tier_key.strip().lower()
        tier: AgentLLMTier | None = None
        try:
            tier = AgentLLMTier(tier_key)
        except ValueError:
            logger.warning(
                "Ignoring unsupported intelligence tier '%s' during agent creation for user %s",
                tier_key,
                request.user.id,
            )

        if tier is not None:
            owner = organization or request.user
            try:
                preferred_llm_tier = resolve_intelligence_tier_for_owner(owner, tier.value)
            except ValueError:
                raise ValidationError("Unsupported intelligence tier selection.")

    with transaction.atomic():
        if preview_creation_allowed and organization is None:
            # Serialize preview creation per user so repeated quick-create requests
            # cannot provision multiple free preview agents before the first commit lands.
            get_user_model().objects.select_for_update().filter(pk=request.user.pk).exists()
            if user_has_existing_personal_agent_for_signup_preview(request.user):
                raise ValidationError(SIGNUP_PREVIEW_EXISTING_AGENT_MESSAGE)

        try:
            provisioning = PersistentAgentProvisioningService.provision(
                user=request.user,
                organization=organization,
                template_code=template_code,
                charter=charter_text,
                preferred_llm_tier=preferred_llm_tier,
                signup_preview_state=get_signup_preview_creation_state(preview_creation_allowed),
            )
        except PersistentAgentProvisioningError as exc:
            error_payload = exc.args[0] if exc.args else "Unable to create agent."
            raise ValidationError(error_payload) from exc

        persistent_agent = provisioning.agent
        applied_schedule = provisioning.applied_schedule
        agent_name = persistent_agent.name

        user_sms_comms_endpoint = None
        user_email_comms_endpoint = None
        user_web_comms_endpoint = None
        agent_sms_endpoint = None
        agent_email_endpoint = None
        agent_web_endpoint = None
        user_web_address = None

        if sms_enabled:
            user_primary_sms = get_user_primary_sms_number(user=request.user)
            user_contact_sms = user_primary_sms.phone_number if user_primary_sms else None

            if user_primary_sms is None:
                raise ValidationError(
                    "You must have a verified phone number to create an agent with SMS contact."
                )

            agent_sms = find_unused_number()

            agent_sms_endpoint = PersistentAgentCommsEndpoint.objects.create(
                owner_agent=persistent_agent,
                channel=CommsChannel.SMS,
                address=agent_sms.phone_number,
                is_primary=sms_preferred,
            )
            PersistentAgentSmsEndpoint.objects.create(
                endpoint=agent_sms_endpoint,
                supports_mms=True,
                carrier_name=agent_sms.provider,
            )

            user_sms_comms_endpoint, _ = PersistentAgentCommsEndpoint.objects.get_or_create(
                channel=CommsChannel.SMS,
                address=user_primary_sms.phone_number,
                defaults={"owner_agent": None},
            )

        if email_enabled:
            agent_email_endpoint = ensure_default_agent_email_endpoint(
                persistent_agent,
                is_primary=preferred_contact_method == "email",
            )

            user_email_comms_endpoint, _ = PersistentAgentCommsEndpoint.objects.get_or_create(
                channel=CommsChannel.EMAIL,
                address=user_contact_email,
                defaults={"owner_agent": None},
            )

        if web_enabled:
            user_web_address = build_web_user_address(request.user.id, persistent_agent.id)
            agent_web_address = build_web_agent_address(persistent_agent.id)
            user_web_comms_endpoint, _ = PersistentAgentCommsEndpoint.objects.get_or_create(
                channel=CommsChannel.WEB,
                address=user_web_address,
                defaults={"owner_agent": None, "is_primary": False},
            )
            agent_web_comms_endpoint, _ = PersistentAgentCommsEndpoint.objects.get_or_create(
                channel=CommsChannel.WEB,
                address=agent_web_address,
                defaults={
                    "owner_agent": persistent_agent,
                    "is_primary": web_preferred,
                },
            )
            web_endpoint_updates = []
            if agent_web_comms_endpoint.owner_agent_id != persistent_agent.id:
                agent_web_comms_endpoint.owner_agent = persistent_agent
                web_endpoint_updates.append("owner_agent")
            if web_preferred and not agent_web_comms_endpoint.is_primary:
                agent_web_comms_endpoint.is_primary = True
                web_endpoint_updates.append("is_primary")
            if web_endpoint_updates:
                agent_web_comms_endpoint.save(update_fields=web_endpoint_updates)
            agent_web_endpoint = agent_web_comms_endpoint

        if sms_preferred:
            preferred_endpoint = user_sms_comms_endpoint
            user_contact = user_contact_sms
            conversation_channel = CommsChannel.SMS.value
            agent_channel_endpoint = agent_sms_endpoint
        elif web_preferred:
            preferred_endpoint = user_web_comms_endpoint
            user_contact = user_web_address
            conversation_channel = CommsChannel.WEB.value
            agent_channel_endpoint = agent_web_endpoint
        else:
            preferred_endpoint = user_email_comms_endpoint
            user_contact = user_contact_email
            conversation_channel = CommsChannel.EMAIL.value
            agent_channel_endpoint = agent_email_endpoint

        if preferred_endpoint is None or not user_contact:
            raise ValidationError("We could not determine your preferred contact channel.")

        persistent_agent.preferred_contact_endpoint = preferred_endpoint
        persistent_agent.save(update_fields=["preferred_contact_endpoint"])

        if sms_enabled and user_contact_sms and agent_sms_endpoint:
            try:
                sms.send_sms(
                    to_number=user_contact_sms,
                    from_number=agent_sms_endpoint.address,
                    body=(
                        "Gobii: You've enabled SMS communication with Gobii. "
                        "Reply HELP for help, STOP to opt-out."
                    ),
                    owner_user=request.user,
                )
            except Exception:
                pass

        conversation = _get_or_create_conversation(
            channel=conversation_channel,
            address=user_contact,
            owner_agent=persistent_agent,
        )

        if user_sms_comms_endpoint:
            _ensure_participant(
                conversation,
                user_sms_comms_endpoint,
                PersistentAgentConversationParticipant.ParticipantRole.EXTERNAL,
            )
        if user_email_comms_endpoint:
            _ensure_participant(
                conversation,
                user_email_comms_endpoint,
                PersistentAgentConversationParticipant.ParticipantRole.EXTERNAL,
            )
        if user_web_comms_endpoint:
            _ensure_participant(
                conversation,
                user_web_comms_endpoint,
                PersistentAgentConversationParticipant.ParticipantRole.HUMAN_USER,
            )
        if agent_channel_endpoint is not None:
            _ensure_participant(
                conversation,
                agent_channel_endpoint,
                PersistentAgentConversationParticipant.ParticipantRole.AGENT,
            )

        initial_message_obj = PersistentAgentMessage.objects.create(
            is_outbound=False,
            from_endpoint=preferred_endpoint,
            to_endpoint=agent_channel_endpoint,
            conversation=conversation,
            body=initial_message,
            owner_agent=persistent_agent,
        )
        if initial_attachments_list:
            _save_attachments(initial_message_obj, initial_attachments_list)

        if selected_template and selected_template.default_tools:
            for tool_name in selected_template.default_tools:
                if isinstance(tool_name, str) and tool_name.startswith(CUSTOM_TOOL_PREFIX):
                    continue
                try:
                    mark_tool_enabled_without_discovery(persistent_agent, tool_name)
                except Exception as exc:
                    logger.warning(
                        "Failed to enable MCP tool '%s' for agent %s: %s",
                        tool_name,
                        persistent_agent.id,
                        exc,
                    )

        _apply_pending_pipedream_app_selections(
            request,
            organization=organization,
            selected_pipedream_app_slugs=selected_pipedream_app_slugs,
        )

        initial_message_id = str(initial_message_obj.id)
        has_initial_attachments = initial_message_obj.attachments.exists()

        def _import_initial_attachments_then_process() -> None:
            try:
                import_message_attachments_to_filespace(initial_message_id)
            except Exception:
                # Attachment import is best-effort; the initial message should still reach the agent.
                logger.exception(
                    "Failed synchronous filespace import for initial agent message %s",
                    initial_message_id,
                )
            finally:
                process_agent_events_task.delay(str(persistent_agent.id))

        if has_initial_attachments:
            transaction.on_commit(_import_initial_attachments_then_process)
        else:
            transaction.on_commit(lambda: process_agent_events_task.delay(str(persistent_agent.id)))

        for key in (
            "agent_charter",
            "agent_charter_source",
            "agent_charter_override",
            AGENT_SELECTED_PIPEDREAM_APP_SLUGS_SESSION_KEY,
            PretrainedWorkerTemplateService.TEMPLATE_SESSION_KEY,
        ):
            if key in request.session:
                del request.session[key]
        clear_trial_onboarding_intent(request)

        base_props = {
            "agent_id": str(persistent_agent.id),
            "agent_name": agent_name,
            "contact_email": user_contact_email or "",
            "contact_sms": user_contact_sms or "",
            "initial_message": initial_message,
            "charter": initial_message or "",
            "preferred_contact_method": preferred_contact_method,
            "template_code": selected_template.code if selected_template else "",
            "template_schedule_applied": applied_schedule or "",
        }
        props = Analytics.with_org_properties(base_props, organization=organization)
        marketing_props = Analytics.with_org_properties(
            {
                "agent_id": str(persistent_agent.id),
                "template_code": selected_template.code if selected_template else "",
                "template_schedule_applied": applied_schedule or "",
            },
            organization=organization,
        )
        transaction.on_commit(
            lambda: Analytics.track_event(
                user_id=request.user.id,
                event=AnalyticsEvent.PERSISTENT_AGENT_CREATED,
                source=AnalyticsSource.WEB,
                properties=props.copy(),
            )
        )
        if preview_creation_allowed:
            preview_props = props.copy()
            preview_props["signup_preview_state"] = persistent_agent.signup_preview_state
            transaction.on_commit(
                lambda: Analytics.track_event(
                    user_id=request.user.id,
                    event=AnalyticsEvent.SIGNUP_PREVIEW_AGENT_CREATED,
                    source=AnalyticsSource.WEB,
                    properties=preview_props.copy(),
                )
            )
        if props.get("organization"):
            transaction.on_commit(
                lambda: Analytics.track_event(
                    user_id=request.user.id,
                    event=AnalyticsEvent.ORGANIZATION_PERSISTENT_AGENT_CREATED,
                    source=AnalyticsSource.WEB,
                    properties=props.copy(),
                )
            )
            transaction.on_commit(
                lambda: Analytics.track_event(
                    user_id=request.user.id,
                    event=AnalyticsEvent.ORGANIZATION_AGENT_CREATED,
                    source=AnalyticsSource.WEB,
                    properties=props.copy(),
                )
            )

        transaction.on_commit(
            lambda: emit_configured_custom_capi_event(
                user=request.user,
                event_name=ConfiguredCustomEvent.AGENT_CREATED,
                plan_owner=organization or request.user,
                properties=marketing_props.copy(),
                request=request,
            )
        )

        return AgentCreationResult(
            agent=persistent_agent,
            organization=organization,
            applied_schedule=applied_schedule,
            template_code=template_code,
            contact_email=user_contact_email,
            contact_sms=user_contact_sms,
            preferred_contact_method=preferred_contact_method,
            initial_message=initial_message,
        )


def enable_agent_sms_contact(agent: PersistentAgent, phone) -> tuple[PersistentAgentCommsEndpoint, PersistentAgentCommsEndpoint]:
    if not phone or not phone.is_verified:
        raise ValidationError("Please verify a phone number before enabling SMS.")
    if agent.sms_disabled:
        raise ValidationError("SMS has been disabled for this agent.")

    with transaction.atomic():
        existing_agent_sms = agent.comms_endpoints.filter(channel=CommsChannel.SMS).first()
        created = False

        if existing_agent_sms:
            agent_sms_endpoint = existing_agent_sms
            updates = []
            if not agent_sms_endpoint.is_primary:
                agent_sms_endpoint.is_primary = True
                updates.append("is_primary")
            if agent_sms_endpoint.owner_agent_id != agent.id:
                agent_sms_endpoint.owner_agent = agent
                updates.append("owner_agent")
            if updates:
                agent_sms_endpoint.save(update_fields=updates)
        else:
            agent_sms = find_unused_number()
            agent_sms_endpoint = PersistentAgentCommsEndpoint.objects.create(
                owner_agent=agent,
                channel=CommsChannel.SMS,
                address=agent_sms.phone_number,
                is_primary=True,
            )
            PersistentAgentSmsEndpoint.objects.create(
                endpoint=agent_sms_endpoint,
                supports_mms=True,
                carrier_name=agent_sms.provider,
            )
            created = True

        user_sms_endpoint, _ = PersistentAgentCommsEndpoint.objects.get_or_create(
            channel=CommsChannel.SMS,
            address=phone.phone_number,
            defaults={"owner_agent": None},
        )

        if agent.preferred_contact_endpoint_id != user_sms_endpoint.id:
            agent.preferred_contact_endpoint = user_sms_endpoint
            agent.save(update_fields=["preferred_contact_endpoint"])

        conversation = _get_or_create_conversation(
            channel=CommsChannel.SMS.value,
            address=phone.phone_number,
            owner_agent=agent,
        )
        _ensure_participant(
            conversation,
            user_sms_endpoint,
            PersistentAgentConversationParticipant.ParticipantRole.EXTERNAL,
        )
        _ensure_participant(
            conversation,
            agent_sms_endpoint,
            PersistentAgentConversationParticipant.ParticipantRole.AGENT,
        )

        if created:
            try:
                sms.send_sms(
                    to_number=phone.phone_number,
                    from_number=agent_sms_endpoint.address,
                    body="Gobii: You've enabled SMS communication with Gobii. Reply HELP for help, STOP to opt-out.",
                    owner_user=agent.user,
                )
            except Exception:
                pass

            PersistentAgentMessage.objects.create(
                is_outbound=False,
                from_endpoint=user_sms_endpoint,
                to_endpoint=agent_sms_endpoint,
                conversation=conversation,
                body=(
                    "Hi I've enabled SMS communication with you! "
                    "Could you introduce yourself and confirm SMS is working?"
                ),
                owner_agent=agent,
            )

        transaction.on_commit(lambda: process_agent_events_task.delay(str(agent.id)))

    return agent_sms_endpoint, user_sms_endpoint
