from __future__ import annotations

from datetime import date, timedelta
from typing import Any

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

from app.modules.clients.models import Client
from app.modules.alerts.workflow_escalations import list_workflow_escalations
from app.modules.core.iam.models import User
from app.modules.core.rbac.models import Permission, Role, RolePermission, UserRole
from app.modules.employees.service import (
    build_employee_scope,
    list_employee_work_assignable_users,
    _engagement_sla,
    _engagement_team,
    _weighted_progress,
)
from app.modules.services.execution import CLOSED_TASK_STATUSES
from app.modules.services.models import ClientServiceSubscription, ClientServiceTaskInstance, ServiceCatalogue, ServiceTaskComment

MANAGER_ROLES = {"Manager", "Branch Manager", "System Admin", "Firm Admin", "Partner"}
MANAGER_PERMISSIONS = {"employees.work.manage", "services.tasks.review", "services.tasks.assign"}
REVIEW_STATUSES = {"completed", "ready_review", "review_pending", "pending_review", "manager_review"}
CLIENT_PENDING_STATUSES = {"blocked", "client_pending", "waiting_client", "documents_pending"}


def _count(db: Session, stmt) -> int:
    return int(db.execute(stmt).scalar() or 0)


def get_user_role_names(db: Session, user_id: int) -> list[str]:
    return list(
        db.execute(
            select(Role.name)
            .join(UserRole, UserRole.role_id == Role.id)
            .where(UserRole.user_id == int(user_id), Role.is_active.is_(True))
            .order_by(Role.name.asc())
        ).scalars().all()
    )


def get_user_permission_names(db: Session, user_id: int) -> set[str]:
    """Return active permission codes granted through the user's active roles.

    The ERP stores permissions in the normalized RolePermission mapping table;
    Role itself intentionally has no ``permissions`` column.
    """
    rows = db.execute(
        select(Permission.code)
        .join(RolePermission, RolePermission.permission_id == Permission.id)
        .join(Role, Role.id == RolePermission.role_id)
        .join(UserRole, UserRole.role_id == Role.id)
        .where(
            UserRole.user_id == int(user_id),
            Role.is_active.is_(True),
            Permission.is_active.is_(True),
        )
        .distinct()
    ).scalars().all()
    return {str(code).strip() for code in rows if code and str(code).strip()}


def can_access_manager_dashboard(db: Session, current_user) -> bool:
    roles = set(get_user_role_names(db, current_user.id))
    if roles.intersection(MANAGER_ROLES):
        return True
    return bool(get_user_permission_names(db, current_user.id).intersection(MANAGER_PERMISSIONS))


def _active_financial_year(request) -> str | None:
    value = request.session.get("active_financial_year") or getattr(request.state, "year_code", None)
    value = (value or "").strip()
    return value or None


def _scope(db: Session, request, current_user):
    return build_employee_scope(
        db,
        current_user,
        tenant_id=request.session.get("active_tenant_id") or getattr(current_user, "tenant_id", None),
        branch_id=request.session.get("active_branch_id"),
    )


def _task_scope(stmt, scope, fy: str | None = None):
    stmt = stmt.where(
        ClientServiceTaskInstance.tenant_id == scope.tenant_id,
        ClientServiceTaskInstance.is_active.is_(True),
    )
    if getattr(scope, "branch_id", None) is not None:
        stmt = stmt.where(ClientServiceTaskInstance.branch_id == scope.branch_id)
    if fy:
        stmt = stmt.where(ClientServiceTaskInstance.financial_year == fy)
    return stmt


def _client_scope(stmt, scope):
    stmt = stmt.where(Client.tenant_id == scope.tenant_id, Client.is_active.is_(True))
    if getattr(scope, "branch_id", None) is not None:
        stmt = stmt.where(Client.branch_id == scope.branch_id)
    return stmt


def _is_open(status: str | None) -> bool:
    return (status or "pending").strip().lower() not in CLOSED_TASK_STATUSES


def _status_label(status: str | None) -> str:
    return (status or "pending").replace("_", " ").replace("-", " ").title()




def _days_overdue(due_date) -> int:
    if not due_date:
        return 0
    delta = (date.today() - due_date).days
    return delta if delta > 0 else 0


def _age_bucket(days: int) -> str:
    if days >= 30:
        return "30+ days"
    if days >= 15:
        return "15-29 days"
    if days >= 8:
        return "8-14 days"
    if days >= 4:
        return "4-7 days"
    if days >= 1:
        return "1-3 days"
    return "Current"

def _task_href(task: ClientServiceTaskInstance) -> str:
    task_id = getattr(task, "id", None)
    if task_id:
        return f"/employees/work/tasks/{task_id}/communication"
    return "/manager/work"


def _task_row(task: ClientServiceTaskInstance) -> dict[str, Any]:
    today = date.today()
    client = getattr(task, "client", None)
    catalogue = getattr(task, "catalogue", None)
    subscription = getattr(task, "subscription", None)
    assignee = getattr(task, "assigned_to", None)
    status = (getattr(task, "status", None) or "pending").strip().lower()
    due_date = getattr(task, "internal_target_date", None)
    service_name = getattr(catalogue, "service_name", None) or getattr(catalogue, "name", None)
    if not service_name and subscription is not None:
        sub_catalogue = getattr(subscription, "catalogue", None)
        service_name = getattr(sub_catalogue, "service_name", None) or getattr(sub_catalogue, "name", None)
    return {
        "id": getattr(task, "id", None),
        "subscription_id": getattr(task, "subscription_id", None),
        "client_name": getattr(client, "client_name", None) or "Unlinked Client",
        "client_code": getattr(client, "client_code", None) or "",
        "service_name": service_name or "Service",
        "task_name": getattr(task, "task_name", None) or "Task",
        "period": getattr(task, "financial_year", None) or "-",
        "due_date": due_date,
        "status": status,
        "status_label": _status_label(status),
        "priority": getattr(task, "priority", None) or "normal",
        "assigned_to": getattr(assignee, "full_name", None) or getattr(assignee, "email", None) or "Unassigned",
        "comment_count": len([c for c in getattr(task, "comments", []) if not getattr(c, "is_deleted", False)]),
        "is_overdue": bool(due_date and due_date < today and _is_open(status)),
        "is_due_today": bool(due_date and due_date == today and _is_open(status)),
        "days_overdue": _days_overdue(due_date) if _is_open(status) else 0,
        "age_bucket": _age_bucket(_days_overdue(due_date)) if _is_open(status) else "Closed",
        "href": _task_href(task),
    }


def _load_tasks(db: Session, scope, fy: str | None) -> list[ClientServiceTaskInstance]:
    stmt = (
        select(ClientServiceTaskInstance)
        .options(
            selectinload(ClientServiceTaskInstance.client),
            selectinload(ClientServiceTaskInstance.catalogue),
            selectinload(ClientServiceTaskInstance.assigned_to),
            selectinload(ClientServiceTaskInstance.comments).selectinload(ServiceTaskComment.created_by),
            selectinload(ClientServiceTaskInstance.documents),
            selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.catalogue),
        )
    )
    stmt = _task_scope(stmt, scope, fy)
    stmt = stmt.order_by(
        ClientServiceTaskInstance.internal_target_date.is_(None),
        ClientServiceTaskInstance.internal_target_date.asc(),
        ClientServiceTaskInstance.priority.desc(),
        ClientServiceTaskInstance.id.desc(),
    ).limit(350)
    return list(db.execute(stmt).scalars().all())


def _staff_rows(db: Session, scope, task_rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
    try:
        users = list_employee_work_assignable_users(db, scope)
    except Exception:
        stmt = select(User).where(User.tenant_id == scope.tenant_id, User.is_active.is_(True))
        if getattr(scope, "branch_id", None) is not None:
            stmt = stmt.where(User.branch_id == scope.branch_id)
        users = list(db.execute(stmt.order_by(User.full_name.asc(), User.email.asc()).limit(100)).scalars().all())

    stats_by_name: dict[str, dict[str, int]] = {}
    for row in task_rows:
        name = row.get("assigned_to") or "Unassigned"
        bucket = stats_by_name.setdefault(name, {"active": 0, "overdue": 0, "review": 0, "client_pending": 0, "due_today": 0})
        if _is_open(row.get("status")):
            bucket["active"] += 1
        if row.get("is_overdue"):
            bucket["overdue"] += 1
        if row.get("is_due_today"):
            bucket["due_today"] += 1
        if row.get("status") in REVIEW_STATUSES:
            bucket["review"] += 1
        if row.get("status") in CLIENT_PENDING_STATUSES:
            bucket["client_pending"] += 1

    rows: list[dict[str, Any]] = []
    seen = set()
    for user in users:
        name = getattr(user, "full_name", None) or getattr(user, "email", None) or "User"
        seen.add(name)
        stats = stats_by_name.get(name, {"active": 0, "overdue": 0, "review": 0, "client_pending": 0, "due_today": 0})
        total = stats["active"] + stats["review"]
        rows.append({
            "name": name,
            "email": getattr(user, "email", None) or "",
            "designation": getattr(user, "designation", None) or "",
            **stats,
            "load_status": "Heavy" if total >= 40 else ("Balanced" if total >= 10 else "Light"),
        })
    if "Unassigned" in stats_by_name and "Unassigned" not in seen:
        stats = stats_by_name["Unassigned"]
        rows.append({"name": "Unassigned", "email": "", "designation": "Allocation pending", **stats, "load_status": "Needs allocation"})
    rows.sort(key=lambda r: (r["active"] + r["review"], r["overdue"], r["due_today"]), reverse=True)
    return rows[:30]


def _client_rows(db: Session, scope, task_rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
    clients = db.execute(_client_scope(select(Client), scope).order_by(Client.client_name.asc()).limit(100)).scalars().all()
    by_client: dict[str, dict[str, int]] = {}
    for row in task_rows:
        key = row["client_name"]
        bucket = by_client.setdefault(key, {"open": 0, "overdue": 0, "client_pending": 0, "review": 0})
        if _is_open(row.get("status")):
            bucket["open"] += 1
        if row.get("is_overdue"):
            bucket["overdue"] += 1
        if row.get("status") in CLIENT_PENDING_STATUSES:
            bucket["client_pending"] += 1
        if row.get("status") in REVIEW_STATUSES:
            bucket["review"] += 1
    out: list[dict[str, Any]] = []
    for client in clients:
        name = getattr(client, "client_name", None) or "Client"
        stats = by_client.get(name, {"open": 0, "overdue": 0, "client_pending": 0, "review": 0})
        out.append({
            "id": getattr(client, "id", None),
            "client_name": name,
            "client_code": getattr(client, "client_code", None) or "",
            "pan": getattr(client, "pan", None) or "",
            "gstin": getattr(client, "gstin", None) or "",
            **stats,
            "href": f"/clients/{getattr(client, 'id', '')}/edit" if getattr(client, "id", None) else "/clients",
        })
    return out[:50]


def _report_cards() -> list[dict[str, str]]:
    return [
        {"group": "Execution", "title": "Team Work Status", "desc": "Open, overdue, in-progress and blocked assignments for the active branch/team.", "href": "/manager/dashboard?tab=team-work"},
        {"group": "Due Control", "title": "Due Calendar", "desc": "Due today, due this week and overdue work in one control view.", "href": "/manager/dashboard?tab=due-calendar"},
        {"group": "Review", "title": "Manager Review Queue", "desc": "Completed or review-ready work waiting for manager action.", "href": "/manager/dashboard?tab=review-queue"},
        {"group": "Client Follow-up", "title": "Client Pending Report", "desc": "Tasks blocked due to documents, clarification or client data pending.", "href": "/manager/dashboard?tab=client-pending"},
        {"group": "Escalation", "title": "Escalation Register", "desc": "Unassigned, overdue and aged client-pending items requiring manager intervention.", "href": "/manager/dashboard?tab=escalations"},
        {"group": "Team", "title": "Staff Workload Report", "desc": "Staff-wise active, overdue, review and client-pending workload.", "href": "/manager/dashboard?tab=team-work"},
        {"group": "Documents", "title": "Document Checklist Report", "desc": "Document-related blocked tasks and communication timeline shortcuts.", "href": "/manager/dashboard?tab=documents"},
    ]


def _wizard_cards() -> list[dict[str, str]]:
    return [
        {"title": "Task Assignment Wizard", "desc": "Allocate unassigned tasks and rebalance heavy workload using the existing manager work board.", "href": "/manager/work"},
        {"title": "Due Control Wizard", "desc": "Review due today, due this week and overdue items before escalation.", "href": "/manager/dashboard?tab=due-calendar"},
        {"title": "Review Wizard", "desc": "Open review-ready tasks and proceed through the existing task communication timeline.", "href": "/manager/dashboard?tab=review-queue"},
        {"title": "Client Query Wizard", "desc": "Handle client-pending items and record clarification/document requirements.", "href": "/manager/dashboard?tab=client-pending"},
        {"title": "Document Checklist Wizard", "desc": "Verify document-related blocked tasks and open the existing documents area.", "href": "/documents"},
        {"title": "Escalation Wizard", "desc": "Review aged overdue, unassigned and client-pending items requiring partner/branch attention.", "href": "/manager/dashboard?tab=escalations"},
        {"title": "Work Closure Wizard", "desc": "Open engagement progress and close work after review completion.", "href": "/employees/progress"},
        {"title": "Alert / Follow-up Wizard", "desc": "Open alerts for reminders, escalations and follow-up actions.", "href": "/alerts"},
    ]


def _advanced_engagement_rows(tasks: list[ClientServiceTaskInstance]) -> list[dict[str, Any]]:
    grouped: dict[int, list[ClientServiceTaskInstance]] = {}
    for task in tasks:
        grouped.setdefault(int(task.subscription_id), []).append(task)
    rows: list[dict[str, Any]] = []
    for subscription_id, group in grouped.items():
        first = group[0]
        subscription = getattr(first, "subscription", None)
        weighted = _weighted_progress(group)
        sla = _engagement_sla(subscription, group)
        team = _engagement_team(subscription)
        status_values = {(getattr(t, "status", None) or "pending").strip().lower() for t in group}
        completed = sum(1 for t in group if (getattr(t, "status", None) or "pending").strip().lower() in CLOSED_TASK_STATUSES)
        review_pending = sum(1 for t in group if (getattr(t, "manager_review_status", None) or "") == "pending" or (getattr(t, "partner_review_status", None) or "") == "pending" or (getattr(t, "review_partner_review_status", None) or "") == "pending")
        if completed == len(group) and group:
            status = "completed"
        elif "blocked" in status_values or getattr(subscription, "workflow_pause_reason", None):
            status = "blocked"
        elif "in_progress" in status_values or completed:
            status = "in_progress"
        else:
            status = "pending"
        rows.append({
            "subscription_id": subscription_id,
            "client_name": getattr(getattr(first, "client", None), "client_name", None) or "Client",
            "service_name": getattr(getattr(first, "catalogue", None), "service_name", None) or "Service",
            "financial_year": getattr(first, "financial_year", None) or "-",
            "status": status,
            "task_count": len(group),
            "completed_count": completed,
            "review_pending": review_pending,
            "progress_percent": weighted["progress_percent"],
            "completed_weight": weighted["completed_weight"],
            "total_weight": weighted["total_weight"],
            "sla": sla,
            "team": team,
            "manager_name": next((m["name"] for m in team if m["role"] == "Manager"), "Unassigned"),
            "staff_name": next((m["name"] for m in team if m["role"] == "Primary Staff"), "Unassigned"),
            "partner_name": next((m["name"] for m in team if m["role"] == "Engagement Partner"), "Unassigned"),
            "href": f"/employee/work/engagements/{subscription_id}",
        })
    rows.sort(key=lambda r: (r["sla"]["status"] == "breached", r["review_pending"], -r["progress_percent"]), reverse=True)
    return rows


def _capacity_rows(tasks: list[ClientServiceTaskInstance]) -> list[dict[str, Any]]:
    by_user: dict[str, dict[str, Any]] = {}
    for task in tasks:
        user = getattr(task, "assigned_to", None)
        name = getattr(user, "full_name", None) or getattr(user, "email", None) or "Unassigned"
        row = by_user.setdefault(name, {"name": name, "open_weight": 0, "open_tasks": 0, "overdue": 0, "review": 0, "engagement_ids": set()})
        status = (getattr(task, "status", None) or "pending").strip().lower()
        if status not in CLOSED_TASK_STATUSES:
            row["open_tasks"] += 1
            row["open_weight"] += _weighted_progress([task])["total_weight"]
            row["engagement_ids"].add(int(task.subscription_id))
            if getattr(task, "internal_target_date", None) and task.internal_target_date < date.today():
                row["overdue"] += 1
        if status in REVIEW_STATUSES:
            row["review"] += 1
    out=[]
    for row in by_user.values():
        score = row["open_weight"] + row["review"] * 2 + row["overdue"] * 3
        row["engagement_count"] = len(row.pop("engagement_ids"))
        row["capacity_score"] = score
        row["load_status"] = "Heavy" if score >= 55 else ("Balanced" if score >= 18 else "Light")
        out.append(row)
    out.sort(key=lambda r: r["capacity_score"], reverse=True)
    return out


def build_manager_dashboard_payload(db: Session, request, current_user) -> dict[str, Any]:
    scope = _scope(db, request, current_user)
    fy = _active_financial_year(request)
    today = date.today()
    next_week = today + timedelta(days=7)
    tasks = _load_tasks(db, scope, fy)
    task_rows = [_task_row(t) for t in tasks]
    open_rows = [r for r in task_rows if _is_open(r["status"])]
    overdue_rows = [r for r in open_rows if r["is_overdue"]]
    due_today_rows = [r for r in open_rows if r["is_due_today"]]
    due_week_rows = [r for r in open_rows if r["due_date"] and today <= r["due_date"] <= next_week]
    review_rows = [r for r in task_rows if r["status"] in REVIEW_STATUSES]
    client_pending_rows = [r for r in open_rows if r["status"] in CLIENT_PENDING_STATUSES]
    unassigned_rows = [r for r in open_rows if (r.get("assigned_to") or "Unassigned") == "Unassigned"]
    in_progress_rows = [r for r in open_rows if r["status"] == "in_progress"]

    staff_rows = _staff_rows(db, scope, task_rows)
    client_rows = _client_rows(db, scope, task_rows)
    advanced_engagements = _advanced_engagement_rows(tasks)
    unified_escalations = list_workflow_escalations(db, tenant_id=getattr(scope, "tenant_id", None), branch_id=getattr(scope, "branch_id", None), assigned_to_user_id=current_user.id)
    capacity_rows = _capacity_rows(tasks)
    sla_breached = [row for row in advanced_engagements if row["sla"]["status"] == "breached"]
    sla_warning = [row for row in advanced_engagements if row["sla"]["status"] in {"critical", "warning"}]
    engagement_review_queue = [row for row in advanced_engagements if row["review_pending"]]

    tenant = getattr(scope, "tenant", None)
    branch = getattr(scope, "branch", None)
    age_buckets = [
        {"label": "1-3 days", "count": len([r for r in overdue_rows if r.get("age_bucket") == "1-3 days"])},
        {"label": "4-7 days", "count": len([r for r in overdue_rows if r.get("age_bucket") == "4-7 days"])},
        {"label": "8-14 days", "count": len([r for r in overdue_rows if r.get("age_bucket") == "8-14 days"])},
        {"label": "15-29 days", "count": len([r for r in overdue_rows if r.get("age_bucket") == "15-29 days"])},
        {"label": "30+ days", "count": len([r for r in overdue_rows if r.get("age_bucket") == "30+ days"])},
    ]

    status_summary: dict[str, int] = {}
    service_summary: dict[str, int] = {}
    for row in open_rows:
        status_summary[row.get("status_label") or "Pending"] = status_summary.get(row.get("status_label") or "Pending", 0) + 1
        service_summary[row.get("service_name") or "Service"] = service_summary.get(row.get("service_name") or "Service", 0) + 1

    escalation_rows = sorted(
        (unassigned_rows + overdue_rows + client_pending_rows),
        key=lambda r: (r.get("days_overdue") or 0, r.get("is_overdue") or False, r.get("due_date") or date.max),
        reverse=True,
    )

    overview = {
        "tenant": tenant,
        "branch": branch,
        "tenant_id": getattr(scope, "tenant_id", None),
        "branch_id": getattr(scope, "branch_id", None),
        "financial_year": fy,
        "total_tasks": len(task_rows),
        "open_task_count": len(open_rows),
        "unassigned_count": len(unassigned_rows),
        "in_progress_count": len(in_progress_rows),
        "overdue_count": len(overdue_rows),
        "due_today_count": len(due_today_rows),
        "due_week_count": len(due_week_rows),
        "client_pending_count": len(client_pending_rows),
        "review_pending_count": len(review_rows),
        "staff_count": len(staff_rows),
        "client_count": len(client_rows),
        "escalation_count": len(escalation_rows),
        "unified_escalation_count": len(unified_escalations),
        "escalations_over_3_days": len([r for r in unified_escalations if r["age_days"] >= 3]),
        "engagement_count": len(advanced_engagements),
        "sla_breached_count": len(sla_breached),
        "sla_warning_count": len(sla_warning),
        "engagement_review_count": len(engagement_review_queue),
        "heavy_capacity_count": len([row for row in capacity_rows if row["load_status"] == "Heavy"]),
        "today": today,
    }

    return {
        "roles": sorted(get_user_role_names(db, current_user.id)),
        "overview": overview,
        "team_work": task_rows[:100],
        "due_today": due_today_rows[:30],
        "due_week": due_week_rows[:30],
        "overdue": overdue_rows[:30],
        "unassigned": unassigned_rows[:30],
        "in_progress": in_progress_rows[:30],
        "review_queue": review_rows[:60],
        "client_pending": client_pending_rows[:60],
        "documents_pending": client_pending_rows[:40],
        "escalations": escalation_rows[:80],
        "unified_escalations": unified_escalations,
        "age_buckets": age_buckets,
        "status_summary": sorted([{"label": k, "count": v} for k, v in status_summary.items()], key=lambda x: x["count"], reverse=True)[:10],
        "service_summary": sorted([{"label": k, "count": v} for k, v in service_summary.items()], key=lambda x: x["count"], reverse=True)[:10],
        "staff_rows": staff_rows,
        "clients": client_rows,
        "advanced_engagements": advanced_engagements,
        "capacity_rows": capacity_rows,
        "sla_breached": sla_breached,
        "sla_warning": sla_warning,
        "engagement_review_queue": engagement_review_queue,
        "reports": _report_cards(),
        "wizards": _wizard_cards(),
    }


# ---------------------------------------------------------------------------
# Phase 5A - engagement-level Manager review workspace.
# This is a presentation and navigation layer over the existing task review,
# evidence, comment, rework, AQMM and lock controls. No duplicate review state
# or review table is introduced.
# ---------------------------------------------------------------------------
_MANAGER_REVIEW_PENDING = {"pending", "review_pending", "pending_review", "manager_review", "ready_review"}
_MANAGER_REVIEW_DONE = {"reviewed"}
_MANAGER_REVIEW_REWORK = {"rework_required"}


def _manager_review_task_state(task: ClientServiceTaskInstance) -> str:
    manager_status = (getattr(task, "manager_review_status", None) or "not_required").strip().lower()
    task_status = (getattr(task, "status", None) or "pending").strip().lower()
    rework_status = (getattr(task, "rework_status", None) or "none").strip().lower()
    if manager_status in _MANAGER_REVIEW_REWORK or rework_status == "open":
        return "rework"
    if manager_status in _MANAGER_REVIEW_DONE:
        return "reviewed"
    if manager_status in _MANAGER_REVIEW_PENDING or task_status in REVIEW_STATUSES:
        return "pending_review"
    if getattr(task, "aqmm_manager_review_required", False) and task_status in CLOSED_TASK_STATUSES:
        return "pending_review"
    return "not_ready"


def _manager_review_category(task: ClientServiceTaskInstance) -> str:
    return (getattr(task, "task_category", None) or "General Workflow").strip() or "General Workflow"


def _manager_review_task_row(task: ClientServiceTaskInstance) -> dict[str, Any]:
    state = _manager_review_task_state(task)
    comments = [c for c in (getattr(task, "comments", None) or []) if not getattr(c, "is_deleted", False)]
    documents = list(getattr(task, "documents", None) or [])
    assignee = getattr(task, "assigned_to", None)
    return {
        "id": int(task.id),
        "task_name": getattr(task, "task_name", None) or "Task",
        "description": getattr(task, "description", None) or "",
        "category": _manager_review_category(task),
        "sequence_no": int(getattr(task, "sequence_no", 0) or 0),
        "task_status": (getattr(task, "status", None) or "pending").strip().lower(),
        "task_status_label": _status_label(getattr(task, "status", None)),
        "review_state": state,
        "review_state_label": state.replace("_", " ").title(),
        "manager_review_status": (getattr(task, "manager_review_status", None) or "not_required").strip().lower(),
        "manager_review_note": getattr(task, "manager_review_note", None) or "",
        "rework_status": (getattr(task, "rework_status", None) or "none").strip().lower(),
        "rework_reason": getattr(task, "rework_reason", None) or "",
        "assigned_to": getattr(assignee, "full_name", None) or getattr(assignee, "email", None) or "Unassigned",
        "response_type": (getattr(task, "response_type", None) or "NONE").strip().upper(),
        "response_required": bool(getattr(task, "response_required", False)),
        "checklist_response": getattr(task, "checklist_response", None),
        "checklist_text_response": getattr(task, "checklist_text_response", None),
        "checklist_number_response": getattr(task, "checklist_number_response", None),
        "checklist_date_response": getattr(task, "checklist_date_response", None),
        "checklist_remarks": getattr(task, "checklist_remarks", None) or "",
        "evidence_required": bool(getattr(task, "evidence_required", False) or getattr(task, "aqmm_evidence_required", False)),
        "evidence_count": len(documents),
        "comment_count": len(comments),
        "comments": comments[:20],
        "is_aqmm_task": bool(getattr(task, "is_aqmm_task", False)),
        "aqmm_reference": getattr(task, "aqmm_reference", None) or "",
        "blocks_final_release": bool(getattr(task, "aqmm_blocks_final_release", False)),
        "is_locked": bool(getattr(task, "is_locked", False) or getattr(getattr(task, "subscription", None), "is_locked", False)),
        "documents_href": f"/documents/tasks/{task.id}",
        "task_href": f"/services/work-tracker/tasks/{task.id}/edit",
    }


def get_manager_review_workspace(
    db: Session,
    request,
    current_user,
    *,
    subscription_id: int,
    active_task_id: int | None = None,
) -> dict[str, Any] | None:
    """Build one Manager review workspace for an engagement in active scope."""
    scope = _scope(db, request, current_user)
    fy = _active_financial_year(request)
    stmt = (
        select(ClientServiceTaskInstance)
        .options(
            selectinload(ClientServiceTaskInstance.client),
            selectinload(ClientServiceTaskInstance.catalogue),
            selectinload(ClientServiceTaskInstance.assigned_to),
            selectinload(ClientServiceTaskInstance.comments).selectinload(ServiceTaskComment.created_by),
            selectinload(ClientServiceTaskInstance.documents),
            selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.catalogue),
        )
        .where(ClientServiceTaskInstance.subscription_id == int(subscription_id))
    )
    stmt = _task_scope(stmt, scope, fy)
    tasks = list(db.execute(stmt.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())).scalars().all())
    if not tasks:
        return None

    task_rows = [_manager_review_task_row(task) for task in tasks]
    reviewable = [row for row in task_rows if row["review_state"] in {"pending_review", "reviewed", "rework"}]
    pending = [row for row in reviewable if row["review_state"] == "pending_review"]
    reviewed = [row for row in reviewable if row["review_state"] == "reviewed"]
    rework = [row for row in reviewable if row["review_state"] == "rework"]

    categories_by_name: dict[str, dict[str, Any]] = {}
    for row in task_rows:
        category = categories_by_name.setdefault(row["category"], {
            "name": row["category"], "tasks": [], "total": 0, "reviewable": 0,
            "pending": 0, "reviewed": 0, "rework": 0,
        })
        category["tasks"].append(row)
        category["total"] += 1
        if row["review_state"] in {"pending_review", "reviewed", "rework"}:
            category["reviewable"] += 1
            category[row["review_state"].replace("pending_review", "pending")] += 1
    categories = list(categories_by_name.values())
    for category in categories:
        denominator = category["reviewable"] or 0
        category["progress_percent"] = round(category["reviewed"] * 100 / denominator) if denominator else 0
        if category["rework"]:
            category["status"] = "rework"
        elif category["pending"]:
            category["status"] = "pending_review"
        elif denominator and category["reviewed"] == denominator:
            category["status"] = "reviewed"
        else:
            category["status"] = "not_ready"

    active = None
    if active_task_id:
        active = next((row for row in task_rows if row["id"] == int(active_task_id)), None)
    if active is None:
        active = (pending or rework or reviewed or task_rows)[0]

    first = tasks[0]
    subscription = getattr(first, "subscription", None)
    weighted = _weighted_progress(tasks)
    sla = _engagement_sla(subscription, tasks)
    team = _engagement_team(subscription)
    return {
        "subscription_id": int(subscription_id),
        "client_name": getattr(getattr(first, "client", None), "client_name", None) or "Client",
        "service_name": getattr(getattr(first, "catalogue", None), "service_name", None) or "Service",
        "financial_year": getattr(first, "financial_year", None) or "-",
        "weighted_progress": weighted,
        "sla": sla,
        "team": team,
        "categories": categories,
        "tasks": task_rows,
        "active_task": active,
        "reviewable_count": len(reviewable),
        "pending_review_count": len(pending),
        "reviewed_count": len(reviewed),
        "rework_count": len(rework),
        "is_locked": bool(getattr(subscription, "is_locked", False)),
    }


def get_next_manager_review_task_id(workspace: dict[str, Any], current_task_id: int) -> int | None:
    """Return the next task requiring Manager action, preserving category/sequence order."""
    tasks = workspace.get("tasks") or []
    current_index = next((i for i, row in enumerate(tasks) if int(row["id"]) == int(current_task_id)), -1)
    ordered = tasks[current_index + 1:] + tasks[:max(current_index, 0)]
    next_row = next((row for row in ordered if row["review_state"] == "pending_review"), None)
    return int(next_row["id"]) if next_row else None
