from __future__ import annotations

import re
from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session, selectinload

from app.modules.core.tenancy.models import Branch
from app.modules.services.models import (
    FirmServiceSelection,
    FirmServiceTaskTemplate,
    ServiceCatalogue,
    ServiceCategory,
    ServiceDefaultTaskTemplate,
    ServiceDueDateRule,
)

RECURRENCE_CHOICES = [
    ("one_time", "One Time"),
    ("monthly", "Monthly"),
    ("quarterly", "Quarterly"),
    ("yearly", "Yearly"),
    ("event_based", "Event Based"),
    ("custom", "Custom"),
]

ENGAGEMENT_TYPE_CHOICES = [
    ("assurance", "Assurance"),
    ("non_assurance", "Non-Assurance"),
]

VALID_ENGAGEMENT_TYPES = {value for value, _label in ENGAGEMENT_TYPE_CHOICES}


def normalize_engagement_type(value: str | None) -> str:
    value = (value or "").strip().lower().replace("-", "_").replace(" ", "_")
    if value in {"assurance", "audit", "aud", "certification", "certificate", "attestation"}:
        return "assurance"
    if value in {"non_assurance", "nonassurance", "non_audit", "nonaudit", "non", "compliance", "consulting", "consultancy"}:
        return "non_assurance"
    return "non_assurance"


def engagement_type_label(value: str | None) -> str:
    normalized = normalize_engagement_type(value)
    return "Assurance" if normalized == "assurance" else "Non-Assurance"


def normalize_code(value: str) -> str:
    value = (value or "").strip().upper()
    value = re.sub(r"[^A-Z0-9]+", "-", value)
    value = re.sub(r"-+", "-", value).strip("-")
    return value


def list_categories(db: Session, *, q: str = ""):
    query = select(ServiceCategory)
    if q.strip():
        term = f"%{q.strip()}%"
        query = query.where(or_(ServiceCategory.code.ilike(term), ServiceCategory.name.ilike(term)))
    return db.execute(query.order_by(ServiceCategory.sort_order.asc(), ServiceCategory.name.asc())).scalars().all()


def get_category(db: Session, category_id: int) -> ServiceCategory | None:
    return db.execute(select(ServiceCategory).where(ServiceCategory.id == category_id)).scalar_one_or_none()


def list_catalogue_payload(db: Session, *, q: str = "", category_id: int | None = None, recurrence_type: str = "", engagement_type: str = "", page: int = 1, per_page: int = 20):
    # Keep catalogue paging predictable and prevent invalid/very large requests.
    page = max(1, int(page or 1))
    allowed_page_sizes = {20, 50, 100}
    per_page = int(per_page or 20)
    if per_page not in allowed_page_sizes:
        per_page = 20

    query = select(ServiceCatalogue).where(
        ServiceCatalogue.is_active.is_(True)
    ).options(
        selectinload(ServiceCatalogue.service_category),
        selectinload(ServiceCatalogue.default_task_templates),
        selectinload(ServiceCatalogue.due_date_rules),
    )
    if q.strip():
        term = f"%{q.strip()}%"
        query = query.outerjoin(ServiceCategory, ServiceCategory.id == ServiceCatalogue.category_id).where(
            or_(
                ServiceCatalogue.service_code.ilike(term),
                ServiceCatalogue.service_name.ilike(term),
                ServiceCatalogue.category.ilike(term),
                ServiceCategory.name.ilike(term),
            )
        )
    if category_id:
        query = query.where(ServiceCatalogue.category_id == category_id)
    if recurrence_type.strip():
        query = query.where(ServiceCatalogue.recurrence_type == recurrence_type.strip())
    if engagement_type.strip():
        query = query.where(ServiceCatalogue.engagement_type == normalize_engagement_type(engagement_type))

    total = db.execute(select(func.count()).select_from(query.subquery())).scalar_one()
    pages = max(1, (total + per_page - 1) // per_page)
    page = min(page, pages)

    rows = db.execute(
        query.order_by(ServiceCatalogue.sort_order.asc(), ServiceCatalogue.service_name.asc())
        .offset((page - 1) * per_page)
        .limit(per_page)
    ).scalars().all()

    start_item = ((page - 1) * per_page + 1) if total else 0
    end_item = min(page * per_page, total)

    return {
        "rows": rows,
        "q": q,
        "category_id": category_id,
        "recurrence_type": recurrence_type,
        "engagement_type": engagement_type,
        "page": page,
        "per_page": per_page,
        "total": total,
        "pages": pages,
        "start_item": start_item,
        "end_item": end_item,
    }


def list_firm_services_payload(db: Session, *, tenant_id: int, q: str = ""):
    query = (
        select(FirmServiceSelection, ServiceCatalogue, Branch)
        .join(ServiceCatalogue, ServiceCatalogue.id == FirmServiceSelection.service_catalogue_id)
        .outerjoin(Branch, Branch.id == FirmServiceSelection.default_branch_id)
        .where(
            FirmServiceSelection.tenant_id == tenant_id,
            FirmServiceSelection.is_enabled.is_(True),
            ServiceCatalogue.is_active.is_(True),
        )
    )
    if q.strip():
        term = f"%{q.strip()}%"
        query = query.outerjoin(ServiceCategory, ServiceCategory.id == ServiceCatalogue.category_id).where(
            or_(
                ServiceCatalogue.service_code.ilike(term),
                ServiceCatalogue.service_name.ilike(term),
                ServiceCatalogue.category.ilike(term),
                ServiceCategory.name.ilike(term),
            )
        )
    rows = db.execute(query.order_by(ServiceCatalogue.sort_order.asc(), ServiceCatalogue.service_name.asc())).all()
    return [
        {"selection": selection, "catalogue": catalogue, "branch": branch}
        for selection, catalogue, branch in rows
    ]


def list_disabled_catalogues(db: Session, *, tenant_id: int, q: str = ""):
    enabled_subq = (
        select(FirmServiceSelection.service_catalogue_id)
        .where(
            FirmServiceSelection.tenant_id == tenant_id,
            FirmServiceSelection.is_enabled.is_(True),
        )
    )

    query = select(ServiceCatalogue).where(
        ServiceCatalogue.is_active.is_(True),
        ~ServiceCatalogue.id.in_(enabled_subq),
    ).options(
        selectinload(ServiceCatalogue.service_category),
        selectinload(ServiceCatalogue.default_task_templates),
        selectinload(ServiceCatalogue.due_date_rules),
    )
    if q.strip():
        term = f"%{q.strip()}%"
        query = query.outerjoin(ServiceCategory, ServiceCategory.id == ServiceCatalogue.category_id).where(
            or_(
                ServiceCatalogue.service_code.ilike(term),
                ServiceCatalogue.service_name.ilike(term),
                ServiceCatalogue.category.ilike(term),
                ServiceCategory.name.ilike(term),
            )
        )
    return db.execute(query.order_by(ServiceCatalogue.sort_order.asc(), ServiceCatalogue.service_name.asc())).scalars().all()


def get_catalogue(db: Session, catalogue_id: int) -> ServiceCatalogue | None:
    return db.execute(
        select(ServiceCatalogue)
        .options(
            selectinload(ServiceCatalogue.service_category),
            selectinload(ServiceCatalogue.default_task_templates),
        )
        .where(ServiceCatalogue.id == catalogue_id)
    ).scalar_one_or_none()


def get_firm_selection(db: Session, *, tenant_id: int, catalogue_id: int) -> FirmServiceSelection | None:
    return db.execute(
        select(FirmServiceSelection).where(
            FirmServiceSelection.tenant_id == tenant_id,
            FirmServiceSelection.service_catalogue_id == catalogue_id,
        )
    ).scalar_one_or_none()


def get_firm_task_templates(db: Session, *, tenant_id: int, catalogue_id: int):
    return db.execute(
        select(FirmServiceTaskTemplate)
        .where(
            FirmServiceTaskTemplate.tenant_id == tenant_id,
            FirmServiceTaskTemplate.service_catalogue_id == catalogue_id,
        )
        .order_by(FirmServiceTaskTemplate.sequence_no.asc(), FirmServiceTaskTemplate.id.asc())
    ).scalars().all()


def get_default_task_templates(db: Session, *, catalogue_id: int):
    return db.execute(
        select(ServiceDefaultTaskTemplate)
        .where(ServiceDefaultTaskTemplate.service_catalogue_id == catalogue_id)
        .order_by(ServiceDefaultTaskTemplate.sequence_no.asc(), ServiceDefaultTaskTemplate.id.asc())
    ).scalars().all()


def next_task_sequence(db: Session, *, tenant_id: int, catalogue_id: int) -> int:
    max_seq = db.execute(
        select(func.max(FirmServiceTaskTemplate.sequence_no)).where(
            FirmServiceTaskTemplate.tenant_id == tenant_id,
            FirmServiceTaskTemplate.service_catalogue_id == catalogue_id,
        )
    ).scalar_one()
    return int(max_seq or 0) + 1


def next_default_task_sequence(db: Session, *, catalogue_id: int) -> int:
    max_seq = db.execute(
        select(func.max(ServiceDefaultTaskTemplate.sequence_no)).where(
            ServiceDefaultTaskTemplate.service_catalogue_id == catalogue_id,
        )
    ).scalar_one()
    return int(max_seq or 0) + 1

def get_firm_task_template(
    db: Session,
    *,
    tenant_id: int,
    catalogue_id: int,
    task_id: int,
) -> FirmServiceTaskTemplate | None:
    return db.execute(
        select(FirmServiceTaskTemplate).where(
            FirmServiceTaskTemplate.id == task_id,
            FirmServiceTaskTemplate.tenant_id == tenant_id,
            FirmServiceTaskTemplate.service_catalogue_id == catalogue_id,
        )
    ).scalar_one_or_none()


def get_default_task_template(
    db: Session,
    *,
    catalogue_id: int,
    task_id: int,
) -> ServiceDefaultTaskTemplate | None:
    return db.execute(
        select(ServiceDefaultTaskTemplate).where(
            ServiceDefaultTaskTemplate.id == task_id,
            ServiceDefaultTaskTemplate.service_catalogue_id == catalogue_id,
        )
    ).scalar_one_or_none()
