from dataclasses import dataclass
from datetime import date
from typing import Any

from django.contrib.auth import get_user_model
from django.db import transaction
from django.utils import timezone

from billing.services import BillingDomainError, generate_service_invoice
from customers.models import (
    BillingProfile,
    Customer,
    Dealer,
    FeasibilityAssessment,
    Inquiry,
    InternetPackage,
    NotificationPreference,
    ServiceAccount,
)
from inventory.models import DeviceAssignment
from inventory.services import (
    InventoryCustodyError,
    assign_device_to_service,
)
from network.models import (
    NetworkAssignment,
    ProvisioningRequest,
)
from network.services import (
    NetworkAssignmentError,
    create_activation_network_request,
)
from tenancy.models import Organization
from tenancy.services import record_audit_log


User = get_user_model()


class CustomerActivationError(Exception):
    pass


class InquiryDomainError(Exception):
    pass


class FeasibilityDomainError(Exception):
    pass


class DealerDomainError(Exception):
    pass


@dataclass(frozen=True)
class CustomerActivationResult:
    customer: Customer
    service_account: ServiceAccount
    billing_profile: BillingProfile
    notification_preference: NotificationPreference
    network_assignment: NetworkAssignment | None = None
    provisioning_request: ProvisioningRequest | None = None
    device_assignment: DeviceAssignment | None = None


def _lock_organization_for_numbering(*, organization: Organization) -> Organization:
    """Serialize tenant-local customer/service number allocation."""
    return (
        Organization.objects
        .select_for_update()
        .get(id=organization.id)
    )


def generate_inquiry_number(*, organization: Organization) -> str:
    prefix = organization.code.upper()[:12]
    sequence = Inquiry.objects.for_organization(organization).count() + 1
    return f"{prefix}-INQ-{sequence:05d}"


def generate_feasibility_number(*, organization: Organization) -> str:
    prefix = organization.code.upper()[:12]
    sequence = FeasibilityAssessment.objects.for_organization(organization).count() + 1
    return f"{prefix}-FSB-{sequence:05d}"


def generate_dealer_code(*, organization: Organization) -> str:
    prefix = organization.code.upper()[:12]
    sequence = Dealer.objects.for_organization(organization).count() + 1
    return f"{prefix}-DLR-{sequence:04d}"


def _build_customer_number(*, organization: Organization) -> str:
    prefix = organization.code.upper()[:12]

    last_customer = (
        Customer.objects.for_organization(organization)
        .order_by("-created_at")
        .first()
    )

    sequence = Customer.objects.for_organization(organization).count() + 1

    if last_customer is not None:
        sequence = max(sequence, 1)

    return f"{prefix}-CUST-{sequence:06d}"


def _build_service_number(*, organization: Organization) -> str:
    prefix = organization.code.upper()[:12]

    sequence = (
        ServiceAccount.objects.for_organization(organization).count() + 1
    )

    return f"{prefix}-SRV-{sequence:06d}"



def _first_month_billing_dates(*, activated_at, due_day: int) -> tuple[date, date]:
    """Create the first invoice in the customer's activation month.

    The first invoice is generated immediately for the activation month. The
    next monthly billing run then starts the normal calendar-month cycle.
    The due date cannot be earlier than the activation date.
    """
    activation_date = timezone.localtime(activated_at).date()
    last_day = __import__("calendar").monthrange(
        activation_date.year,
        activation_date.month,
    )[1]
    safe_due_day = min(due_day, last_day)
    due_date = date(
        activation_date.year,
        activation_date.month,
        safe_due_day,
    )

    if due_date < activation_date:
        due_date = activation_date

    return activation_date, due_date


@transaction.atomic
def activate_customer_service(
    *,
    organization: Organization,
    actor: User,
    internet_package_id,
    dealer_id=None,
    first_name: str,
    last_name: str = "",
    phone: str,
    alternate_phone: str = "",
    email: str = "",
    address_line: str,
    area: str = "",
    city: str,
    billing_day: int,
    due_day: int,
    sms_enabled: bool = True,
    whatsapp_enabled: bool = True,
    network_node_id=None,
    network_username: str = "",
    network_ip_address: str | None = None,
    device_id=None,
    device_assignment_notes: str = "",
    provisioning_payload: dict[str, Any] | None = None,
    activation_metadata: dict[str, Any] | None = None,
) -> CustomerActivationResult:
    if not organization.is_active:
        raise CustomerActivationError("Organization is inactive.")

    # Lock the tenant before allocating customer/service numbers.
    # Billing uses the same organization-first lock order, preventing races
    # between concurrent activations and billing operations.
    organization = _lock_organization_for_numbering(organization=organization)

    first_name = first_name.strip()
    last_name = last_name.strip()
    phone = phone.strip()
    alternate_phone = alternate_phone.strip()
    email = email.strip().lower()
    address_line = address_line.strip()
    area = area.strip()
    city = city.strip()
    device_assignment_notes = device_assignment_notes.strip()

    if not first_name:
        raise CustomerActivationError("Customer first name is required.")

    if not phone:
        raise CustomerActivationError("Customer phone is required.")

    if not address_line:
        raise CustomerActivationError("Service address is required.")

    if not city:
        raise CustomerActivationError("Customer city is required.")

    if billing_day < 1 or billing_day > 28:
        raise CustomerActivationError("Billing day must be between 1 and 28.")

    if due_day < 1 or due_day > 28:
        raise CustomerActivationError("Due day must be between 1 and 28.")

    dealer = None
    if dealer_id:
        try:
            dealer = Dealer.objects.for_organization(organization).get(id=dealer_id)
        except Dealer.DoesNotExist as exc:
            raise CustomerActivationError("Dealer was not found for this organization.") from exc

    try:
        internet_package = (
            InternetPackage.objects
            .for_organization(organization)
            .get(id=internet_package_id, is_active=True)
        )
    except InternetPackage.DoesNotExist as exc:
        raise CustomerActivationError(
            "Active internet package was not found for this organization."
        ) from exc

    duplicate_phone_exists = (
        Customer.objects
        .for_organization(organization)
        .filter(phone=phone)
        .exists()
    )

    if duplicate_phone_exists:
        raise CustomerActivationError(
            "A customer with this phone already exists in this organization."
        )

    customer = Customer.objects.create(
        organization=organization,
        customer_number=_build_customer_number(organization=organization),
        dealer=dealer,
        first_name=first_name,
        last_name=last_name,
        phone=phone,
        alternate_phone=alternate_phone,
        email=email,
        address_line=address_line,
        area=area,
        city=city,
        is_active=True,
    )

    service_account = ServiceAccount.objects.create(
        organization=organization,
        service_number=_build_service_number(organization=organization),
        customer=customer,
        internet_package=internet_package,
        status=ServiceAccount.Status.ACTIVE,
        activated_at=timezone.now(),
    )

    network_assignment = None
    provisioning_request = None
    device_assignment = None

    if network_node_id is not None:
        try:
            network_result = create_activation_network_request(
                organization=organization,
                service_account=service_account,
                network_node_id=network_node_id,
                username=network_username,
                ip_address=network_ip_address,
                provisioning_payload=provisioning_payload,
            )
        except NetworkAssignmentError as exc:
            raise CustomerActivationError(str(exc)) from exc

        network_assignment = network_result.network_assignment
        provisioning_request = network_result.provisioning_request

    if device_id is not None:
        try:
            inventory_result = assign_device_to_service(
                organization=organization,
                actor=actor,
                device_id=device_id,
                service_account_id=service_account.id,
                assignment_notes=device_assignment_notes,
            )
        except InventoryCustodyError as exc:
            raise CustomerActivationError(str(exc)) from exc

        device_assignment = inventory_result.assignment

    billing_profile = BillingProfile.objects.create(
        organization=organization,
        service_account=service_account,
        billing_cycle=BillingProfile.BillingCycle.MONTHLY,
        billing_day=billing_day,
        due_day=due_day,
    )

    notification_preference = NotificationPreference.objects.create(
        organization=organization,
        customer=customer,
        sms_enabled=sms_enabled,
        whatsapp_enabled=whatsapp_enabled,
    )

    # Business rule: an activated customer receives the bill for the same
    # calendar month immediately. The regular monthly billing command will
    # generate the following month and later months. The unique billing-period
    # constraint prevents the first invoice from being generated twice.
    activation_date, first_due_date = _first_month_billing_dates(
        activated_at=service_account.activated_at,
        due_day=due_day,
    )

    try:
        generate_service_invoice(
            organization=organization,
            actor=actor,
            service_account_id=service_account.id,
            billing_period_start=activation_date.replace(day=1),
            billing_period_end=activation_date.replace(
                day=__import__("calendar").monthrange(
                    activation_date.year,
                    activation_date.month,
                )[1]
            ),
            issue_date=activation_date,
            due_date=first_due_date,
        )
    except BillingDomainError as exc:
        raise CustomerActivationError(
            f"Customer was not activated because the first monthly invoice could not be generated: {exc}"
        ) from exc

    record_audit_log(
        organization=organization,
        actor=actor,
        action="CUSTOMER_SERVICE_ACTIVATED",
        resource_type="ServiceAccount",
        resource_id=service_account.id,
        metadata={
            "customer_id": str(customer.id),
            "customer_number": customer.customer_number,
            "service_number": service_account.service_number,
            "internet_package_id": str(internet_package.id),
            "network_assignment_id": (
                str(network_assignment.id) if network_assignment else None
            ),
            "provisioning_request_id": (
                str(provisioning_request.id)
                if provisioning_request
                else None
            ),
            "device_assignment_id": (
                str(device_assignment.id) if device_assignment else None
            ),
            "device_id": (
                str(device_assignment.device_id)
                if device_assignment
                else None
            ),
            "activation_date": activation_date.isoformat(),
            "first_invoice_month": activation_date.strftime("%Y-%m"),
            **(activation_metadata or {}),
        },
    )

    return CustomerActivationResult(
        customer=customer,
        service_account=service_account,
        billing_profile=billing_profile,
        notification_preference=notification_preference,
        network_assignment=network_assignment,
        provisioning_request=provisioning_request,
        device_assignment=device_assignment,
    )


@transaction.atomic
def convert_inquiry_to_customer(
    *,
    inquiry_id,
    organization: Organization,
    actor: User,
    internet_package_id=None,
    billing_day: int = 1,
    due_day: int = 10,
    sms_enabled: bool = True,
    whatsapp_enabled: bool = True,
    network_node_id=None,
    network_username: str = "",
    network_ip_address: str | None = None,
    device_id=None,
    device_assignment_notes: str = "",
) -> CustomerActivationResult:
    try:
        inquiry = (
            Inquiry.objects.for_organization(organization)
            .select_for_update()
            .get(id=inquiry_id)
        )
    except Inquiry.DoesNotExist as exc:
        raise InquiryDomainError("Inquiry was not found for this organization.") from exc

    if inquiry.status == Inquiry.Status.CONVERTED:
        raise InquiryDomainError("This inquiry has already been converted to a customer.")

    if inquiry.status == Inquiry.Status.CANCELLED:
        raise InquiryDomainError("Cannot convert a cancelled inquiry.")

    name_parts = inquiry.full_name.strip().split(" ", 1)
    first_name = name_parts[0]
    last_name = name_parts[1] if len(name_parts) > 1 else ""

    pkg_id = internet_package_id or (inquiry.preferred_package_id if inquiry.preferred_package else None)
    if not pkg_id:
        raise InquiryDomainError("An internet package must be selected for conversion.")

    result = activate_customer_service(
        organization=organization,
        actor=actor,
        internet_package_id=pkg_id,
        dealer_id=inquiry.dealer_id if inquiry.dealer else None,
        first_name=first_name,
        last_name=last_name,
        phone=inquiry.phone,
        alternate_phone=inquiry.alternate_phone,
        email=inquiry.email,
        address_line=inquiry.address_line,
        area=inquiry.area,
        city=inquiry.city,
        billing_day=billing_day,
        due_day=due_day,
        sms_enabled=sms_enabled,
        whatsapp_enabled=whatsapp_enabled,
        network_node_id=network_node_id,
        network_username=network_username,
        network_ip_address=network_ip_address,
        device_id=device_id,
        device_assignment_notes=device_assignment_notes,
    )

    inquiry.converted_customer = result.customer
    inquiry.converted_at = timezone.now()
    inquiry.status = Inquiry.Status.CONVERTED
    inquiry.save(update_fields=["converted_customer", "converted_at", "status", "updated_at"])

    record_audit_log(
        organization=organization,
        actor=actor,
        action="INQUIRY_CONVERTED",
        resource_type="Inquiry",
        resource_id=str(inquiry.id),
        metadata={
            "customer_id": str(result.customer.id),
            "customer_number": result.customer.customer_number,
            "service_number": result.service_account.service_number,
        },
    )

    return result

