from __future__ import annotations

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

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

from app.modules.clients import repository as client_repository
from app.modules.clients.models import Client
from app.modules.consultants.models import ClientConsultantLink, ConsultantProfile
from app.modules.core.rbac.deps import get_user_roles
from app.modules.documents.models import EngagementDocument, PermanentClientDocument
from app.modules.services.execution import (
    CLOSED_TASK_STATUSES,
    TASK_COMMENT_TYPES,
    TASK_COMMENT_VISIBILITIES,
    TASK_PRIORITIES,
    TASK_STATUSES,
    add_task_comment,
)
from app.modules.services.models import (
    ClientServiceSubscription,
    ClientServiceTaskInstance,
    ServiceTaskComment,
    ServiceTaskDocumentRequest,
)

MANAGEMENT_ROLES = {"System Admin", "Firm Admin"}
PARTNER_ROLES = {"Partner"}
MANAGER_ROLES = {"Manager", "Branch Manager"}
STAFF_ROLES = {"Staff", "Employee"}
CLIENT_ROLES = {"Client"}
CONSULTANT_ROLES = {"Consultant"}


@dataclass(frozen=True)
class WorkAccess:
    role_context: str
    can_update_tasks: bool
    can_comment: bool
    allowed_comment_types: list[tuple[str, str]]
    allowed_visibilities: list[tuple[str, str]]
    back_url: str
    can_view_assignee: bool = True
    can_view_document_requests: bool = True
    can_create_document_requests: bool = False
    can_update_document_requests: bool = False
    can_view_engagement_documents: bool = True
    can_view_permanent_documents: bool = True


def _roles(db: Session, user) -> set[str]:
    return set(get_user_roles(db, user.id))


def _safe_int(value: Any) -> int | None:
    try:
        return int(value) if value not in (None, "", "None") else None
    except Exception:
        return None


def _current_client_row(db: Session, user):
    try:
        return client_repository.get_portal_client_for_user(db, user=user)
    except Exception:
        return None


def _current_consultant(db: Session, user) -> ConsultantProfile | None:
    return db.execute(
        select(ConsultantProfile).where(
            ConsultantProfile.user_id == user.id,
            ConsultantProfile.tenant_id == user.tenant_id,
            ConsultantProfile.is_active.is_(True),
        )
    ).scalar_one_or_none()


def _consultant_link_for_engagement(db: Session, *, consultant: ConsultantProfile, engagement: ClientServiceSubscription) -> ClientConsultantLink | None:
    today = date.today()
    return db.execute(
        select(ClientConsultantLink).where(
            ClientConsultantLink.tenant_id == consultant.tenant_id,
            ClientConsultantLink.client_id == engagement.client_id,
            ClientConsultantLink.consultant_id == consultant.id,
            ClientConsultantLink.is_active.is_(True),
            ClientConsultantLink.can_view_engagements.is_(True),
            or_(ClientConsultantLink.service_catalogue_id.is_(None), ClientConsultantLink.service_catalogue_id == engagement.service_catalogue_id),
            or_(ClientConsultantLink.effective_from.is_(None), ClientConsultantLink.effective_from <= today),
            or_(ClientConsultantLink.effective_to.is_(None), ClientConsultantLink.effective_to >= today),
        )
    ).scalars().first()


def _consultant_can_view_engagement(db: Session, *, consultant: ConsultantProfile, engagement: ClientServiceSubscription) -> bool:
    return _consultant_link_for_engagement(db, consultant=consultant, engagement=engagement) is not None


def _is_assigned_staff(engagement: ClientServiceSubscription, tasks: list[ClientServiceTaskInstance], user) -> bool:
    if _safe_int(getattr(engagement, "assigned_staff_user_id", None)) == user.id:
        return True
    return any(_safe_int(getattr(task, "assigned_to_user_id", None)) == user.id for task in tasks)


def _is_manager_for_engagement(engagement: ClientServiceSubscription, user) -> bool:
    return _safe_int(getattr(engagement, "assigned_manager_user_id", None)) == user.id


def _is_partner_for_engagement(engagement: ClientServiceSubscription, user) -> bool:
    return user.id in {
        _safe_int(getattr(engagement, "assigned_partner_user_id", None)),
        _safe_int(getattr(engagement, "performing_partner_user_id", None)),
        _safe_int(getattr(engagement, "review_partner_user_id", None)),
    }


def _engagement_query(engagement_id: int):
    return (
        select(ClientServiceSubscription)
        .options(
            selectinload(ClientServiceSubscription.client),
            selectinload(ClientServiceSubscription.catalogue),
            selectinload(ClientServiceSubscription.assigned_partner),
            selectinload(ClientServiceSubscription.assigned_manager),
            selectinload(ClientServiceSubscription.assigned_staff),
            selectinload(ClientServiceSubscription.review_partner),
        )
        .where(ClientServiceSubscription.id == int(engagement_id), ClientServiceSubscription.is_active.is_(True))
    )


def _load_tasks(db: Session, engagement: ClientServiceSubscription) -> list[ClientServiceTaskInstance]:
    return db.execute(
        select(ClientServiceTaskInstance)
        .options(
            selectinload(ClientServiceTaskInstance.assigned_to),
            selectinload(ClientServiceTaskInstance.assigned_consultant),
            selectinload(ClientServiceTaskInstance.catalogue),
            selectinload(ClientServiceTaskInstance.comments).selectinload(ServiceTaskComment.created_by),
        )
        .where(
            ClientServiceTaskInstance.tenant_id == engagement.tenant_id,
            ClientServiceTaskInstance.subscription_id == engagement.id,
            ClientServiceTaskInstance.is_active.is_(True),
        )
        .order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())
    ).scalars().all()


def _load_documents(db: Session, engagement: ClientServiceSubscription, access: WorkAccess) -> tuple[list[EngagementDocument], list[PermanentClientDocument]]:
    engagement_documents = db.execute(
        select(EngagementDocument)
        .options(selectinload(EngagementDocument.versions))
        .where(
            EngagementDocument.tenant_id == engagement.tenant_id,
            EngagementDocument.client_id == engagement.client_id,
            EngagementDocument.engagement_id == engagement.id,
            EngagementDocument.is_deleted.is_(False),
        )
        .order_by(EngagementDocument.document_type.asc(), EngagementDocument.title.asc())
    ).unique().scalars().all()
    if not access.can_view_engagement_documents:
        engagement_documents = []
    permanent_documents = db.execute(
        select(PermanentClientDocument)
        .options(selectinload(PermanentClientDocument.versions))
        .where(
            PermanentClientDocument.tenant_id == engagement.tenant_id,
            PermanentClientDocument.client_id == engagement.client_id,
            PermanentClientDocument.is_deleted.is_(False),
        )
        .order_by(PermanentClientDocument.category.asc(), PermanentClientDocument.title.asc())
        .limit(50)
    ).unique().scalars().all()
    if not access.can_view_permanent_documents:
        permanent_documents = []
    return engagement_documents, permanent_documents


def _load_timeline(db: Session, engagement: ClientServiceSubscription, access: WorkAccess) -> list[ServiceTaskComment]:
    stmt = (
        select(ServiceTaskComment)
        .options(selectinload(ServiceTaskComment.created_by), selectinload(ServiceTaskComment.task))
        .where(
            ServiceTaskComment.tenant_id == engagement.tenant_id,
            ServiceTaskComment.subscription_id == engagement.id,
            ServiceTaskComment.is_deleted.is_(False),
        )
    )
    if access.role_context == "client":
        stmt = stmt.where(ServiceTaskComment.visibility == "client")
    elif access.role_context == "consultant":
        stmt = stmt.where(ServiceTaskComment.visibility == "consultant")
    return db.execute(stmt.order_by(ServiceTaskComment.created_at_utc.asc(), ServiceTaskComment.id.asc())).scalars().all()


def _build_access(db: Session, *, user, engagement: ClientServiceSubscription, tasks: list[ClientServiceTaskInstance]) -> WorkAccess | None:
    roles = _roles(db, user)
    if roles.intersection(MANAGEMENT_ROLES):
        return WorkAccess("admin", True, True, TASK_COMMENT_TYPES, TASK_COMMENT_VISIBILITIES, "/services/work-tracker", can_create_document_requests=True, can_update_document_requests=True)

    if roles.intersection(CLIENT_ROLES):
        client_row = _current_client_row(db, user)
        if client_row and int(client_row.get("id") or 0) == engagement.client_id and int(client_row.get("tenant_id") or 0) == engagement.tenant_id:
            return WorkAccess("client", False, True, [("client_clarification", "Client Clarification")], [("client", "Client")], "/client/compliance")
        return None

    if roles.intersection(CONSULTANT_ROLES):
        consultant = _current_consultant(db, user)
        if consultant:
            link = _consultant_link_for_engagement(db, consultant=consultant, engagement=engagement)
            if link:
                return WorkAccess(
                    "consultant", False, bool(link.can_reply_to_clarifications),
                    [("consultant_clarification", "Consultant Clarification")], [("consultant", "Consultant")], "/consultant/work",
                    can_view_assignee=bool(link.can_view_assignee),
                    can_view_document_requests=bool(link.can_view_document_requests),
                    can_update_document_requests=bool(link.can_reply_to_clarifications or link.can_act_for_client),
                    can_view_engagement_documents=bool(link.can_view_final_documents or link.can_upload_documents),
                    can_view_permanent_documents=bool(link.can_view_permanent_documents),
                )
        return None

    if roles.intersection(PARTNER_ROLES) and _is_partner_for_engagement(engagement, user):
        return WorkAccess("partner", True, True, TASK_COMMENT_TYPES, TASK_COMMENT_VISIBILITIES, "/partner/reviews", can_create_document_requests=True, can_update_document_requests=True)

    if roles.intersection(MANAGER_ROLES):
        if _is_manager_for_engagement(engagement, user) or (engagement.tenant_id == user.tenant_id and (engagement.branch_id in (None, user.branch_id))):
            return WorkAccess("manager", True, True, TASK_COMMENT_TYPES, TASK_COMMENT_VISIBILITIES, "/manager/work", can_create_document_requests=True, can_update_document_requests=True)

    if roles.intersection(STAFF_ROLES) or roles.intersection({"Employee"}):
        if _is_assigned_staff(engagement, tasks, user):
            return WorkAccess("staff", True, True, [("internal_note", "Internal Note"), ("client_clarification", "Client Clarification")], [("internal", "Internal"), ("client", "Client")], "/employee/work", can_create_document_requests=True, can_update_document_requests=True)

    return None


def load_unified_engagement_detail(db: Session, *, request, user, engagement_id: int) -> dict[str, Any] | None:
    engagement = db.execute(_engagement_query(engagement_id)).scalar_one_or_none()
    if not engagement:
        return None
    roles = _roles(db, user)
    if "System Admin" not in roles and engagement.tenant_id != user.tenant_id:
        return None

    tasks = _load_tasks(db, engagement)
    access = _build_access(db, user=user, engagement=engagement, tasks=tasks)
    if not access:
        return None

    engagement_documents, permanent_documents = _load_documents(db, engagement, access)
    document_requests = db.execute(
        select(ServiceTaskDocumentRequest)
        .options(selectinload(ServiceTaskDocumentRequest.task), selectinload(ServiceTaskDocumentRequest.requested_by), selectinload(ServiceTaskDocumentRequest.responded_by))
        .where(ServiceTaskDocumentRequest.tenant_id == engagement.tenant_id, ServiceTaskDocumentRequest.subscription_id == engagement.id, ServiceTaskDocumentRequest.is_active.is_(True))
        .order_by(ServiceTaskDocumentRequest.status.asc(), ServiceTaskDocumentRequest.due_date.asc(), ServiceTaskDocumentRequest.id.desc())
    ).scalars().all() if access.can_view_document_requests else []
    timeline = _load_timeline(db, engagement, access)
    today = date.today()
    for task in tasks:
        status = (task.status or "pending").lower()
        task.status_label = dict(TASK_STATUSES).get(status, status.replace("_", " ").title())
        task.priority_label = dict(TASK_PRIORITIES).get(task.priority or "normal", (task.priority or "normal").replace("_", " ").title())
        task.is_closed_display = status in CLOSED_TASK_STATUSES
        task.is_overdue_display = bool(task.internal_target_date and task.internal_target_date < today and status not in CLOSED_TASK_STATUSES)
        task.comments_visible_count = len([c for c in getattr(task, "comments", []) if not getattr(c, "is_deleted", False)])

    status_counts: dict[str, int] = {code: 0 for code, _ in TASK_STATUSES}
    for task in tasks:
        status_counts[(task.status or "pending").lower()] = status_counts.get((task.status or "pending").lower(), 0) + 1

    return {
        "engagement": engagement,
        "tasks": tasks,
        "engagement_documents": engagement_documents,
        "permanent_documents": permanent_documents,
        "timeline": timeline,
        "document_requests": document_requests,
        "linked_consultants": get_linked_consultants_for_engagement(db, engagement=engagement) if access.can_update_tasks else [],
        "access": access,
        "role_context": access.role_context,
        "task_statuses": TASK_STATUSES,
        "task_priorities": TASK_PRIORITIES,
        "status_counts": status_counts,
        "open_task_count": sum(1 for task in tasks if (task.status or "pending").lower() not in CLOSED_TASK_STATUSES),
        "completed_task_count": sum(1 for task in tasks if (task.status or "pending").lower() in CLOSED_TASK_STATUSES),
    }


def get_task_for_action(db: Session, *, user, task_id: int) -> tuple[ClientServiceTaskInstance | None, WorkAccess | None]:
    task = db.execute(
        select(ClientServiceTaskInstance)
        .options(selectinload(ClientServiceTaskInstance.subscription), selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.client))
        .where(ClientServiceTaskInstance.id == int(task_id), ClientServiceTaskInstance.is_active.is_(True))
    ).scalar_one_or_none()
    if not task or not task.subscription:
        return None, None
    detail = load_unified_engagement_detail(db, request=None, user=user, engagement_id=task.subscription_id)
    if not detail:
        return None, None
    return task, detail["access"]


def save_task_status(db: Session, *, task: ClientServiceTaskInstance, status: str, priority: str | None, user_id: int) -> None:
    allowed_statuses = {code for code, _ in TASK_STATUSES}
    allowed_priorities = {code for code, _ in TASK_PRIORITIES}
    clean_status = (status or task.status or "pending").strip().lower()
    clean_priority = (priority or task.priority or "normal").strip().lower()
    if clean_status in allowed_statuses:
        task.status = clean_status
    if clean_priority in allowed_priorities:
        task.priority = clean_priority
    if task.status == "in_progress" and not task.started_at_utc:
        task.started_at_utc = datetime.now(timezone.utc)
    if task.status in CLOSED_TASK_STATUSES and not task.completed_at_utc:
        task.completed_at_utc = datetime.now(timezone.utc)
    task.updated_by_user_id = user_id


def save_task_comment(db: Session, *, task: ClientServiceTaskInstance, access: WorkAccess, comment_type: str, visibility: str, message: str, user_id: int) -> bool:
    allowed_comment_types = {code for code, _ in access.allowed_comment_types}
    allowed_visibilities = {code for code, _ in access.allowed_visibilities}
    clean_type = comment_type if comment_type in allowed_comment_types else next(iter(allowed_comment_types), "internal_note")
    clean_visibility = visibility if visibility in allowed_visibilities else next(iter(allowed_visibilities), "internal")
    row = add_task_comment(db, task=task, comment_type=clean_type, visibility=clean_visibility, message=message, user_id=user_id)
    return row is not None


def create_document_request(db: Session, *, task: ClientServiceTaskInstance, access: WorkAccess, title: str, description: str, requested_from: str, due_date: date | None, request_type: str, user_id: int) -> ServiceTaskDocumentRequest | None:
    if not access.can_create_document_requests or not (title or "").strip():
        return None
    allowed_from = {"client", "consultant", "client_and_consultant"}
    allowed_types = {"document", "clarification", "approval", "information"}
    row = ServiceTaskDocumentRequest(tenant_id=task.tenant_id, branch_id=task.branch_id, subscription_id=task.subscription_id, task_instance_id=task.id, client_id=task.client_id, request_type=request_type if request_type in allowed_types else "document", title=title.strip(), description=(description or "").strip() or None, requested_from=requested_from if requested_from in allowed_from else "client_and_consultant", due_date=due_date, status="pending", requested_by_user_id=user_id)
    db.add(row)
    visibility = "consultant" if row.requested_from == "consultant" else "client"
    if row.requested_from == "client_and_consultant":
        add_task_comment(db, task=task, comment_type="document_request", visibility="client", message=f"{row.title}: {row.description or ''}".strip(), user_id=user_id)
        add_task_comment(db, task=task, comment_type="document_request", visibility="consultant", message=f"{row.title}: {row.description or ''}".strip(), user_id=user_id)
    else:
        add_task_comment(db, task=task, comment_type="document_request", visibility=visibility, message=f"{row.title}: {row.description or ''}".strip(), user_id=user_id)
    return row


def update_document_request(db: Session, *, request_row: ServiceTaskDocumentRequest, access: WorkAccess, status: str, response_note: str, user_id: int) -> bool:
    if not access.can_update_document_requests:
        return False
    allowed = {"pending", "received", "clarification_required", "verified", "rejected", "closed"}
    clean = status if status in allowed else request_row.status
    request_row.status = clean
    request_row.response_note = (response_note or "").strip() or request_row.response_note
    request_row.responded_by_user_id = user_id
    now = datetime.now(timezone.utc)
    if clean in {"received", "verified", "closed"} and not request_row.received_at_utc:
        request_row.received_at_utc = now
    if clean in {"verified", "closed"}:
        request_row.verified_at_utc = now
        request_row.verified_by_user_id = user_id
    return True

CONSULTANT_ASSIGNMENT_STATUSES = {
    "not_applicable", "offered", "accepted", "declined", "in_progress",
    "submitted", "rework_required", "approved", "cancelled",
}


def get_linked_consultants_for_engagement(db: Session, *, engagement: ClientServiceSubscription) -> list[dict]:
    today = date.today()
    rows = db.execute(
        select(ClientConsultantLink, ConsultantProfile)
        .join(ConsultantProfile, ConsultantProfile.id == ClientConsultantLink.consultant_id)
        .where(
            ClientConsultantLink.tenant_id == engagement.tenant_id,
            ClientConsultantLink.client_id == engagement.client_id,
            ClientConsultantLink.is_active.is_(True),
            ConsultantProfile.is_active.is_(True),
            or_(ClientConsultantLink.service_catalogue_id.is_(None), ClientConsultantLink.service_catalogue_id == engagement.service_catalogue_id),
            or_(ClientConsultantLink.effective_from.is_(None), ClientConsultantLink.effective_from <= today),
            or_(ClientConsultantLink.effective_to.is_(None), ClientConsultantLink.effective_to >= today),
        )
        .order_by(ClientConsultantLink.is_primary.desc(), ConsultantProfile.full_name.asc())
    ).all()
    return [{"link": link, "consultant": consultant} for link, consultant in rows]


def assign_task_to_consultant(
    db: Session,
    *,
    task: ClientServiceTaskInstance,
    access: WorkAccess,
    consultant_id: int | None,
    due_date: date | None,
    user_id: int,
) -> bool:
    if not access.can_update_tasks or task.is_locked:
        return False
    if not consultant_id:
        task.execution_mode = "internal"
        task.assigned_consultant_id = None
        task.consultant_assignment_status = "not_applicable"
        task.consultant_due_date = None
        task.consultant_offered_at_utc = None
        task.consultant_accepted_at_utc = None
        task.consultant_declined_at_utc = None
        task.consultant_decline_reason = None
        task.consultant_started_at_utc = None
        task.consultant_submitted_at_utc = None
        task.consultant_submission_note = None
        task.consultant_approved_at_utc = None
        task.consultant_approved_by_user_id = None
        task.updated_by_user_id = user_id
        return True
    consultant = db.get(ConsultantProfile, int(consultant_id))
    if not consultant or not consultant.is_active or consultant.tenant_id != task.tenant_id:
        return False
    engagement = db.get(ClientServiceSubscription, task.subscription_id)
    if not engagement:
        return False
    link = _consultant_link_for_engagement(db, consultant=consultant, engagement=engagement)
    if not link or not link.can_act_for_client:
        return False
    now = datetime.now(timezone.utc)
    task.execution_mode = "consultant"
    task.assigned_consultant_id = consultant.id
    task.assigned_to_user_id = consultant.user_id
    task.consultant_assignment_status = "offered"
    task.consultant_due_date = due_date or task.internal_target_date
    task.consultant_offered_at_utc = now
    task.consultant_accepted_at_utc = None
    task.consultant_declined_at_utc = None
    task.consultant_decline_reason = None
    task.consultant_started_at_utc = None
    task.consultant_submitted_at_utc = None
    task.consultant_submission_note = None
    task.consultant_approved_at_utc = None
    task.consultant_approved_by_user_id = None
    task.status = "pending"
    task.updated_by_user_id = user_id
    add_task_comment(db, task=task, comment_type="assignment_update", visibility="consultant", message=f"Assignment offered to consultant. Due date: {task.consultant_due_date or 'Not specified'}.", user_id=user_id)
    return True


def review_consultant_submission(db: Session, *, task: ClientServiceTaskInstance, access: WorkAccess, action: str, note: str, user_id: int) -> bool:
    if not access.can_update_tasks or task.execution_mode != "consultant" or task.consultant_assignment_status != "submitted" or task.is_locked:
        return False
    action = (action or "").strip().lower()
    note = (note or "").strip()
    now = datetime.now(timezone.utc)
    if action == "approve":
        task.consultant_assignment_status = "approved"
        task.consultant_approved_at_utc = now
        task.consultant_approved_by_user_id = user_id
        task.status = "completed"
        task.completed_at_utc = now
        task.rework_status = "none"
        task.rework_reason = None
        message = "Consultant submission approved by the firm."
    elif action == "rework":
        if not note:
            return False
        task.consultant_assignment_status = "rework_required"
        task.status = "in_progress"
        task.rework_status = "requested"
        task.rework_reason = note
        task.rework_requested_by_user_id = user_id
        task.rework_requested_at_utc = now
        message = f"Rework requested: {note}"
    else:
        return False
    task.updated_by_user_id = user_id
    add_task_comment(db, task=task, comment_type="review_update", visibility="consultant", message=message, user_id=user_id)
    return True
