from __future__ import annotations

from datetime import date, datetime, timezone

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

from app.modules.clients.models import Client
from app.modules.core.iam.models import User
from app.modules.services.models import (
    ClientServiceSubscription,
    ClientServiceTaskInstance,
    ServiceTaskComment,
    FirmServiceTaskTemplate,
    ServiceCatalogue,
)

TASK_STATUSES = [
    ("pending", "Pending"),
    ("in_progress", "In Progress"),
    ("completed", "Completed"),
    ("blocked", "Blocked"),
    ("not_applicable", "Not Applicable"),
    ("cancelled", "Cancelled"),
]

OPEN_TASK_STATUSES = {"pending", "in_progress", "blocked"}
CLOSED_TASK_STATUSES = {"completed", "not_applicable", "cancelled"}


TASK_COMMENT_TYPES = [
    ("internal_note", "Internal Note"),
    ("client_clarification", "Client Clarification"),
    ("consultant_clarification", "Consultant Clarification"),
    ("partner_review_note", "Partner Review Note"),
]

TASK_COMMENT_VISIBILITIES = [
    ("internal", "Internal"),
    ("client", "Client"),
    ("consultant", "Consultant"),
]

TASK_PRIORITIES = [
    ("low", "Low"),
    ("normal", "Normal"),
    ("high", "High"),
    ("urgent", "Urgent"),
]


def _normalise_status(status: str | None) -> str:
    allowed = {code for code, _label in TASK_STATUSES}
    value = (status or "pending").strip().lower()
    return value if value in allowed else "pending"


def _normalise_priority(priority: str | None) -> str:
    allowed = {code for code, _label in TASK_PRIORITIES}
    value = (priority or "normal").strip().lower()
    return value if value in allowed else "normal"


def _normalise_comment_type(comment_type: str | None) -> str:
    allowed = {code for code, _label in TASK_COMMENT_TYPES}
    value = (comment_type or "internal_note").strip().lower()
    return value if value in allowed else "internal_note"


def _normalise_visibility(visibility: str | None) -> str:
    allowed = {code for code, _label in TASK_COMMENT_VISIBILITIES}
    value = (visibility or "internal").strip().lower()
    return value if value in allowed else "internal"


def parse_date_value(value: str | None) -> date | None:
    text = (value or "").strip()
    if not text:
        return None
    return date.fromisoformat(text)


def _default_assignee_for_template(subscription: ClientServiceSubscription, template: FirmServiceTaskTemplate) -> int | None:
    role = (template.default_role_name or "").strip().lower()
    if "partner" in role:
        return subscription.assigned_partner_user_id
    if "manager" in role:
        return subscription.assigned_manager_user_id
    if "staff" in role or "employee" in role:
        return subscription.assigned_staff_user_id
    return subscription.assigned_staff_user_id or subscription.assigned_manager_user_id or subscription.assigned_partner_user_id


def get_subscription_for_execution(db: Session, *, tenant_id: int, subscription_id: int) -> ClientServiceSubscription | None:
    return db.execute(
        select(ClientServiceSubscription)
        .options(
            selectinload(ClientServiceSubscription.client),
            selectinload(ClientServiceSubscription.catalogue),
            selectinload(ClientServiceSubscription.assigned_partner),
            selectinload(ClientServiceSubscription.assigned_manager),
            selectinload(ClientServiceSubscription.assigned_staff),
        )
        .where(
            ClientServiceSubscription.id == subscription_id,
            ClientServiceSubscription.tenant_id == tenant_id,
        )
    ).scalar_one_or_none()


def list_subscription_execution_payload(db: Session, *, tenant_id: int, branch_id: int | None = None, financial_year: str | None = None, q: str = ""):
    query = (
        select(ClientServiceSubscription)
        .options(
            selectinload(ClientServiceSubscription.client),
            selectinload(ClientServiceSubscription.catalogue),
            selectinload(ClientServiceSubscription.assigned_partner),
            selectinload(ClientServiceSubscription.assigned_manager),
            selectinload(ClientServiceSubscription.assigned_staff),
        )
        .where(
            ClientServiceSubscription.tenant_id == tenant_id,
            ClientServiceSubscription.is_active.is_(True),
            ClientServiceSubscription.status == "active",
            ClientServiceSubscription.is_locked.is_(False),
        )
    )
    if financial_year:
        query = query.where(ClientServiceSubscription.financial_year == financial_year.strip())
    if branch_id:
        query = query.where(ClientServiceSubscription.branch_id == branch_id)
    if q.strip():
        term = f"%{q.strip()}%"
        query = (
            query.join(Client, Client.id == ClientServiceSubscription.client_id)
            .join(ServiceCatalogue, ServiceCatalogue.id == ClientServiceSubscription.service_catalogue_id)
            .where(
                or_(
                    Client.client_name.ilike(term),
                    Client.client_code.ilike(term),
                    ServiceCatalogue.service_code.ilike(term),
                    ServiceCatalogue.service_name.ilike(term),
                )
            )
        )

    rows = db.execute(query.order_by(ClientServiceSubscription.id.desc())).scalars().all()
    payload = []
    for sub in rows:
        total = db.execute(
            select(func.count(ClientServiceTaskInstance.id)).where(
                ClientServiceTaskInstance.subscription_id == sub.id,
                ClientServiceTaskInstance.is_active.is_(True),
            )
        ).scalar_one()
        completed = db.execute(
            select(func.count(ClientServiceTaskInstance.id)).where(
                ClientServiceTaskInstance.subscription_id == sub.id,
                ClientServiceTaskInstance.is_active.is_(True),
                ClientServiceTaskInstance.status == "completed",
            )
        ).scalar_one()
        open_tasks = db.execute(
            select(func.count(ClientServiceTaskInstance.id)).where(
                ClientServiceTaskInstance.subscription_id == sub.id,
                ClientServiceTaskInstance.is_active.is_(True),
                ClientServiceTaskInstance.status.in_(list(OPEN_TASK_STATUSES)),
            )
        ).scalar_one()
        payload.append({"subscription": sub, "total_tasks": total, "completed_tasks": completed, "open_tasks": open_tasks})
    return payload


def _default_internal_target_date(subscription: ClientServiceSubscription, template: FirmServiceTaskTemplate) -> date | None:
    # Phase 4B keeps task target dates internal. Existing task templates do not yet have
    # an offset field, so new generated tasks start blank and can be assigned through the tracker.
    return None


def generate_tasks_for_subscription(db: Session, *, subscription: ClientServiceSubscription, user_id: int) -> int:
    templates = db.execute(
        select(FirmServiceTaskTemplate)
        .where(
            FirmServiceTaskTemplate.tenant_id == subscription.tenant_id,
            FirmServiceTaskTemplate.service_catalogue_id == subscription.service_catalogue_id,
            FirmServiceTaskTemplate.is_active.is_(True),
        )
        .order_by(FirmServiceTaskTemplate.sequence_no.asc(), FirmServiceTaskTemplate.id.asc())
    ).scalars().all()

    created = 0
    for template in templates:
        existing = db.execute(
            select(ClientServiceTaskInstance.id).where(
                ClientServiceTaskInstance.subscription_id == subscription.id,
                ClientServiceTaskInstance.firm_task_template_id == template.id,
                ClientServiceTaskInstance.financial_year == subscription.financial_year,
            )
        ).first()
        if existing:
            continue

        db.add(
            ClientServiceTaskInstance(
                tenant_id=subscription.tenant_id,
                branch_id=subscription.branch_id,
                subscription_id=subscription.id,
                client_id=subscription.client_id,
                service_catalogue_id=subscription.service_catalogue_id,
                firm_task_template_id=template.id,
                financial_year=subscription.financial_year,
                assessment_year=subscription.assessment_year,
                task_name=template.task_name,
                description=template.description,
                sequence_no=template.sequence_no,
                default_role_name=template.default_role_name,
                assigned_to_user_id=_default_assignee_for_template(subscription, template),
                internal_target_date=_default_internal_target_date(subscription, template),
                status="pending",
                priority="normal",
                is_active=True,
                created_by_user_id=user_id,
                updated_by_user_id=user_id,
            )
        )
        created += 1
    return created


def _decorate_task_for_tracker(task: ClientServiceTaskInstance, *, today: date) -> ClientServiceTaskInstance:
    target_date = getattr(task, "internal_target_date", None)
    subscription = getattr(task, "subscription", None)
    engagement_due_date = getattr(subscription, "current_due_date", None) if subscription else None
    task.is_task_overdue = bool(target_date and target_date < today and task.status not in CLOSED_TASK_STATUSES)
    task.is_due_today = bool(target_date and target_date == today and task.status not in CLOSED_TASK_STATUSES)
    task.is_engagement_due_overdue = bool(
        engagement_due_date and engagement_due_date < today and task.status not in CLOSED_TASK_STATUSES
    )
    task.tracker_status_label = dict(TASK_STATUSES).get(task.status, task.status)
    task.priority_label = dict(TASK_PRIORITIES).get(task.priority, task.priority)
    return task




def _apply_partner_visibility_filter(query, partner_user_id: int | None):
    if not partner_user_id:
        return query
    return query.where(
        or_(
            ClientServiceTaskInstance.subscription.has(
                ClientServiceSubscription.assigned_partner_user_id == partner_user_id
            ),
            ClientServiceTaskInstance.client.has(Client.partner_id == partner_user_id),
        )
    )

def list_tasks_payload(
    db: Session,
    *,
    tenant_id: int,
    branch_id: int | None = None,
    assigned_to_user_id: int | None = None,
    partner_user_id: int | None = None,
    status: str = "",
    q: str = "",
    include_inactive: bool = False,
    financial_year: str | None = None,
):
    today = date.today()
    special_filter = (status or "").strip().lower()
    query = (
        select(ClientServiceTaskInstance)
        .options(
            selectinload(ClientServiceTaskInstance.client),
            selectinload(ClientServiceTaskInstance.catalogue),
            selectinload(ClientServiceTaskInstance.assigned_to),
            selectinload(ClientServiceTaskInstance.subscription),
        )
        .where(ClientServiceTaskInstance.tenant_id == tenant_id)
    )
    if financial_year:
        query = query.where(ClientServiceTaskInstance.financial_year == financial_year.strip())
    if branch_id:
        query = query.where(ClientServiceTaskInstance.branch_id == branch_id)
    if assigned_to_user_id:
        query = query.where(ClientServiceTaskInstance.assigned_to_user_id == assigned_to_user_id)
    query = _apply_partner_visibility_filter(query, partner_user_id)
    if special_filter and special_filter not in {"overdue", "due_today", "unassigned"}:
        query = query.where(ClientServiceTaskInstance.status == special_filter)
    if special_filter == "overdue":
        query = query.where(
            ClientServiceTaskInstance.internal_target_date.is_not(None),
            ClientServiceTaskInstance.internal_target_date < today,
            ClientServiceTaskInstance.status.notin_(list(CLOSED_TASK_STATUSES)),
        )
    elif special_filter == "due_today":
        query = query.where(
            ClientServiceTaskInstance.internal_target_date == today,
            ClientServiceTaskInstance.status.notin_(list(CLOSED_TASK_STATUSES)),
        )
    elif special_filter == "unassigned":
        query = query.where(ClientServiceTaskInstance.assigned_to_user_id.is_(None))
    if not include_inactive:
        query = query.where(ClientServiceTaskInstance.is_active.is_(True))
    if q.strip():
        term = f"%{q.strip()}%"
        query = (
            query.join(Client, Client.id == ClientServiceTaskInstance.client_id)
            .join(ServiceCatalogue, ServiceCatalogue.id == ClientServiceTaskInstance.service_catalogue_id)
            .where(
                or_(
                    ClientServiceTaskInstance.task_name.ilike(term),
                    Client.client_name.ilike(term),
                    Client.client_code.ilike(term),
                    ServiceCatalogue.service_code.ilike(term),
                    ServiceCatalogue.service_name.ilike(term),
                )
            )
        )
    rows = db.execute(
        query.order_by(
            ClientServiceTaskInstance.internal_target_date.is_(None),
            ClientServiceTaskInstance.internal_target_date.asc(),
            ClientServiceTaskInstance.status.asc(),
            ClientServiceTaskInstance.sequence_no.asc(),
            ClientServiceTaskInstance.id.desc(),
        )
    ).scalars().all()
    return [_decorate_task_for_tracker(task, today=today) for task in rows]


def get_task(
    db: Session,
    *,
    tenant_id: int,
    task_id: int,
    branch_id: int | None = None,
    assigned_to_user_id: int | None = None,
    partner_user_id: int | None = None,
    financial_year: str | None = None,
) -> ClientServiceTaskInstance | None:
    query = (
        select(ClientServiceTaskInstance)
        .options(
            selectinload(ClientServiceTaskInstance.client),
            selectinload(ClientServiceTaskInstance.catalogue),
            selectinload(ClientServiceTaskInstance.assigned_to),
            selectinload(ClientServiceTaskInstance.subscription),
        )
        .where(
            ClientServiceTaskInstance.id == task_id,
            ClientServiceTaskInstance.tenant_id == tenant_id,
        )
    )
    if branch_id:
        query = query.where(ClientServiceTaskInstance.branch_id == branch_id)
    if financial_year:
        query = query.where(ClientServiceTaskInstance.financial_year == financial_year.strip())
    if assigned_to_user_id:
        query = query.where(ClientServiceTaskInstance.assigned_to_user_id == assigned_to_user_id)
    query = _apply_partner_visibility_filter(query, partner_user_id)
    task = db.execute(query).scalar_one_or_none()
    return _decorate_task_for_tracker(task, today=date.today()) if task else None


def list_assignees_for_execution(db: Session, *, tenant_id: int, branch_id: int | None = None):
    query = select(User).where(User.tenant_id == tenant_id, User.is_active.is_(True))
    if branch_id:
        query = query.where((User.branch_id == branch_id) | (User.branch_id.is_(None)))
    return db.execute(query.order_by(User.full_name.asc(), User.email.asc())).scalars().all()


def apply_task_update(
    task: ClientServiceTaskInstance,
    *,
    status: str,
    priority: str,
    assigned_to_user_id: int | None,
    internal_target_date: date | None,
    remarks: str,
    is_active: bool,
    user_id: int,
):
    if getattr(task, "is_locked", False) or getattr(getattr(task, "subscription", None), "is_locked", False):
        return
    previous_status = task.status
    task.status = _normalise_status(status)
    task.priority = _normalise_priority(priority)
    task.assigned_to_user_id = assigned_to_user_id
    task.internal_target_date = internal_target_date
    task.remarks = remarks.strip() or None
    task.is_active = is_active
    task.updated_by_user_id = user_id

    now = datetime.now(timezone.utc)
    if previous_status != "in_progress" and task.status == "in_progress" and not task.started_at_utc:
        task.started_at_utc = now
    if task.status == "completed" and not task.completed_at_utc:
        task.completed_at_utc = now
    if task.status != "completed":
        task.completed_at_utc = None


def apply_bulk_task_update(
    tasks: list[ClientServiceTaskInstance],
    *,
    status: str | None,
    assigned_to_user_id: int | None,
    update_assignee: bool,
    internal_target_date: date | None,
    update_internal_target_date: bool,
    user_id: int,
) -> tuple[int, int]:
    updated = 0
    skipped = 0
    for task in tasks:
        if getattr(task, "is_locked", False) or getattr(getattr(task, "subscription", None), "is_locked", False):
            skipped += 1
            continue
        previous_status = task.status
        if status:
            task.status = _normalise_status(status)
        if update_assignee:
            task.assigned_to_user_id = assigned_to_user_id
        if update_internal_target_date:
            task.internal_target_date = internal_target_date
        task.updated_by_user_id = user_id
        now = datetime.now(timezone.utc)
        if previous_status != "in_progress" and task.status == "in_progress" and not task.started_at_utc:
            task.started_at_utc = now
        if task.status == "completed" and not task.completed_at_utc:
            task.completed_at_utc = now
        if task.status != "completed":
            task.completed_at_utc = None
        updated += 1
    return updated, skipped


def get_tasks_for_bulk_update(
    db: Session,
    *,
    tenant_id: int,
    task_ids: list[int],
    branch_id: int | None = None,
    assigned_to_user_id: int | None = None,
    partner_user_id: int | None = None,
    financial_year: str | None = None,
) -> list[ClientServiceTaskInstance]:
    if not task_ids:
        return []
    query = (
        select(ClientServiceTaskInstance)
        .options(selectinload(ClientServiceTaskInstance.subscription))
        .where(
            ClientServiceTaskInstance.tenant_id == tenant_id,
            ClientServiceTaskInstance.id.in_(task_ids),
            ClientServiceTaskInstance.is_active.is_(True),
        )
    )
    if branch_id:
        query = query.where(ClientServiceTaskInstance.branch_id == branch_id)
    if financial_year:
        query = query.where(ClientServiceTaskInstance.financial_year == financial_year.strip())
    if assigned_to_user_id:
        query = query.where(ClientServiceTaskInstance.assigned_to_user_id == assigned_to_user_id)
    query = _apply_partner_visibility_filter(query, partner_user_id)
    return db.execute(query).scalars().all()



def list_task_comments(db: Session, *, tenant_id: int, task_id: int) -> list[ServiceTaskComment]:
    return db.execute(
        select(ServiceTaskComment)
        .options(selectinload(ServiceTaskComment.created_by))
        .where(
            ServiceTaskComment.tenant_id == tenant_id,
            ServiceTaskComment.task_instance_id == task_id,
            ServiceTaskComment.is_deleted.is_(False),
        )
        .order_by(ServiceTaskComment.created_at_utc.desc(), ServiceTaskComment.id.desc())
    ).scalars().all()


def add_task_comment(
    db: Session,
    *,
    task: ClientServiceTaskInstance,
    comment_type: str,
    visibility: str,
    message: str,
    user_id: int,
) -> ServiceTaskComment | None:
    if getattr(task, "is_locked", False) or getattr(getattr(task, "subscription", None), "is_locked", False):
        return None
    clean_message = (message or "").strip()
    if not clean_message:
        return None
    row = ServiceTaskComment(
        tenant_id=task.tenant_id,
        branch_id=task.branch_id,
        subscription_id=task.subscription_id,
        task_instance_id=task.id,
        comment_type=_normalise_comment_type(comment_type),
        visibility=_normalise_visibility(visibility),
        message=clean_message,
        created_by_user_id=user_id,
    )
    db.add(row)
    task.updated_by_user_id = user_id
    return row



def list_client_visible_task_comments(
    db: Session,
    *,
    tenant_id: int,
    client_id: int,
    limit: int = 20,
) -> list[ServiceTaskComment]:
    """Return client-visible task communication for one client dashboard.

    This is intentionally read-only and scoped by tenant + client. Internal and
    consultant-only notes are never returned to the client portal.
    """
    safe_limit = max(1, min(int(limit or 20), 100))
    rows = db.execute(
        select(ServiceTaskComment)
        .options(
            selectinload(ServiceTaskComment.created_by),
            selectinload(ServiceTaskComment.task).selectinload(ClientServiceTaskInstance.catalogue),
            selectinload(ServiceTaskComment.task).selectinload(ClientServiceTaskInstance.subscription),
        )
        .join(ClientServiceTaskInstance, ClientServiceTaskInstance.id == ServiceTaskComment.task_instance_id)
        .where(
            ServiceTaskComment.tenant_id == tenant_id,
            ServiceTaskComment.visibility == "client",
            ServiceTaskComment.is_deleted.is_(False),
            ClientServiceTaskInstance.client_id == client_id,
            ClientServiceTaskInstance.tenant_id == tenant_id,
            ClientServiceTaskInstance.is_active.is_(True),
        )
        .order_by(ServiceTaskComment.created_at_utc.desc(), ServiceTaskComment.id.desc())
        .limit(safe_limit)
    ).scalars().all()

    type_labels = dict(TASK_COMMENT_TYPES)
    visibility_labels = dict(TASK_COMMENT_VISIBILITIES)
    for row in rows:
        row.comment_type_label = type_labels.get(row.comment_type, row.comment_type)
        row.visibility_label = visibility_labels.get(row.visibility, row.visibility)
    return rows


def dashboard_stats(
    db: Session,
    *,
    tenant_id: int,
    branch_id: int | None = None,
    assigned_to_user_id: int | None = None,
    partner_user_id: int | None = None,
    financial_year: str | None = None,
):
    today = date.today()
    base = select(ClientServiceTaskInstance).where(
        ClientServiceTaskInstance.tenant_id == tenant_id,
        ClientServiceTaskInstance.is_active.is_(True),
    )
    if branch_id:
        base = base.where(ClientServiceTaskInstance.branch_id == branch_id)
    if financial_year:
        base = base.where(ClientServiceTaskInstance.financial_year == financial_year.strip())
    if assigned_to_user_id:
        base = base.where(ClientServiceTaskInstance.assigned_to_user_id == assigned_to_user_id)
    base = _apply_partner_visibility_filter(base, partner_user_id)

    subq = base.subquery()
    total = db.execute(select(func.count()).select_from(subq)).scalar_one()
    pending = db.execute(select(func.count()).select_from(subq).where(subq.c.status == "pending")).scalar_one()
    progress = db.execute(select(func.count()).select_from(subq).where(subq.c.status == "in_progress")).scalar_one()
    blocked = db.execute(select(func.count()).select_from(subq).where(subq.c.status == "blocked")).scalar_one()
    completed = db.execute(select(func.count()).select_from(subq).where(subq.c.status == "completed")).scalar_one()
    not_applicable = db.execute(select(func.count()).select_from(subq).where(subq.c.status == "not_applicable")).scalar_one()
    unassigned = db.execute(select(func.count()).select_from(subq).where(subq.c.assigned_to_user_id.is_(None))).scalar_one()
    overdue = db.execute(
        select(func.count()).select_from(subq).where(
            subq.c.internal_target_date.is_not(None),
            subq.c.internal_target_date < today,
            subq.c.status.notin_(list(CLOSED_TASK_STATUSES)),
        )
    ).scalar_one()
    due_today = db.execute(
        select(func.count()).select_from(subq).where(
            subq.c.internal_target_date == today,
            subq.c.status.notin_(list(CLOSED_TASK_STATUSES)),
        )
    ).scalar_one()
    return {
        "total": total,
        "pending": pending,
        "in_progress": progress,
        "blocked": blocked,
        "completed": completed,
        "not_applicable": not_applicable,
        "unassigned": unassigned,
        "overdue": overdue,
        "due_today": due_today,
    }
