from __future__ import annotations

from datetime import date, datetime, timezone

from fastapi import APIRouter, Form, Request
from fastapi.responses import JSONResponse, RedirectResponse
from sqlalchemy import func, or_, select
from sqlalchemy.orm import selectinload

from app.core.db.common import CommonSessionLocal
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
from app.core.security.session_auth import get_current_user
from app.core.templating import templates
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.core.rbac.permission_guard import require_permission
from app.modules.core.tenancy.year_control import redirect_if_financial_year_locked
from app.modules.services.client_services import (
    SUBSCRIPTION_STATUSES,
    assessment_year_from_financial_year,
    attach_engagement_to_plan,
    ensure_engagement_quality_workflow,
    enforce_quality_gate_on_subscription,
    get_enabled_firm_service,
    get_existing_subscription,
    get_or_create_client_service_plan,
    list_assignable_users,
    list_clients_for_assignment,
    list_enabled_services_for_assignment,
    list_review_partners,
    normalize_financial_year,
    normalize_period_label,
    parse_date,
    period_choices_for_service,
    recurrence_requires_period,
    review_partner_required_for_engagement,
)
from app.modules.clients.models import Client, ClientBusinessUnit, ClientBranch
from app.modules.registrations.models import ClientRegistration, RegistrationType
from app.modules.services.due_dates import apply_due_date_rule_to_subscription
from app.modules.services.execution import generate_tasks_for_subscription_if_ready
from app.modules.services.scope_targets import list_scope_targets, resolve_scope_target
from app.modules.services.models import (
    ClientServicePlan,
    ClientServiceSubscription,
    ClientServiceTaskInstance,
)

router = APIRouter(prefix="/services/subscriptions", tags=["client-service-subscriptions-ui"])


def _tenant_id(request, user):
    return int(
        request.session.get("active_tenant_id")
        or request.session.get("selected_tenant_id")
        or request.session.get("tenant_id")
        or user.tenant_id
    )


def _branch_id(request, user):
    value = request.session.get("active_branch_id")
    if value in (None, "", 0, "0"):
        return int(getattr(user, "branch_id", 0) or 0) or None
    return int(value)



def _active_financial_year(request: Request) -> str:
    return normalize_financial_year(
        request.session.get("active_financial_year")
        or getattr(request.state, "year_code", None)
    )


def _partner_scope_id(db, user) -> int | None:
    roles = set(get_user_roles(db, user.id))
    return int(user.id) if "Partner" in roles else None


def _registration_scope_context(db, *, tenant_id: int, plan: ClientServicePlan):
    businesses = db.execute(
        select(ClientBusinessUnit).where(
            ClientBusinessUnit.tenant_id == tenant_id,
            ClientBusinessUnit.client_id == plan.client_id,
            ClientBusinessUnit.is_active.is_(True),
        ).order_by(
            ClientBusinessUnit.is_primary.desc(),
            ClientBusinessUnit.business_name,
        )
    ).scalars().all()

    branches = db.execute(
        select(ClientBranch).where(
            ClientBranch.tenant_id == tenant_id,
            ClientBranch.client_id == plan.client_id,
            ClientBranch.is_active.is_(True),
        ).order_by(
            ClientBranch.is_primary.desc(),
            ClientBranch.branch_name,
        )
    ).scalars().all()

    registration_rows = db.execute(
        select(ClientRegistration, RegistrationType)
        .join(
            RegistrationType,
            RegistrationType.id == ClientRegistration.registration_type_id,
        )
        .where(
            ClientRegistration.tenant_id == tenant_id,
            ClientRegistration.client_id == plan.client_id,
            ClientRegistration.status.in_(("active", "valid", "registered")),
            RegistrationType.is_active.is_(True),
        )
        .order_by(
            RegistrationType.code,
            ClientRegistration.registration_number,
        )
    ).all()

    registrations = []
    registration_type_by_id = {}
    for registration, registration_type in registration_rows:
        registrations.append(registration)
        registration_type_by_id[registration.id] = registration_type

    business_by_id = {row.id: row for row in businesses}
    branch_by_id = {row.id: row for row in branches}
    registration_by_id = {row.id: row for row in registrations}

    return {
        "businesses": businesses,
        "branches": branches,
        "registrations": registrations,
        "registration_type_by_id": registration_type_by_id,
        "scope_business": business_by_id.get(plan.business_unit_id),
        "scope_branch": branch_by_id.get(plan.client_branch_id),
        "scope_registration": registration_by_id.get(plan.registration_id),
        "scope_registration_type": (
            registration_type_by_id.get(plan.registration_id)
            if plan.registration_id
            else None
        ),
    }


def _scope_key_for(scope_type: str, target_id: int) -> str:
    prefixes = {
        "client": "CLIENT",
        "business_unit": "BUSINESS",
        "client_branch": "BRANCH",
        "registration": "REGISTRATION",
    }
    if scope_type not in prefixes:
        raise ValueError("scope_type")
    return f"{prefixes[scope_type]}:{int(target_id)}"

def _ctx(request, db, user, **extra):
    data = {
        "request": request,
        "current_user": user,
        "current_user_roles": get_user_roles(db, user.id),
        "current_user_permissions": get_user_permissions(db, user.id),
        "csrf_token": get_or_create_csrf_token(request),
        "subscription_statuses": SUBSCRIPTION_STATUSES,
    }
    data.update(extra)
    return data


def _denied():
    from app.core.http_responses import ui_access_denied
    return ui_access_denied()


def _load_plan(db, *, plan_id: int, tenant_id: int, branch_id: int | None):
    stmt = (
        select(ClientServicePlan)
        .options(
            selectinload(ClientServicePlan.client),
            selectinload(ClientServicePlan.catalogue),
            selectinload(ClientServicePlan.default_partner),
            selectinload(ClientServicePlan.default_performing_partner),
            selectinload(ClientServicePlan.default_manager),
            selectinload(ClientServicePlan.default_staff),
            selectinload(ClientServicePlan.default_review_partner),
        )
        .where(
            ClientServicePlan.id == plan_id,
            ClientServicePlan.tenant_id == tenant_id,
        )
    )
    if branch_id:
        stmt = stmt.where(ClientServicePlan.branch_id == branch_id)
    return db.execute(stmt).scalar_one_or_none()


def _optional_int(value: str) -> int | None:
    value = (value or "").strip()
    return int(value) if value else None


def _allowed_id(value: int | None, allowed: set[int], field_name: str) -> int | None:
    if value is not None and value not in allowed:
        raise ValueError(field_name)
    return value


def _engagement_is_open(row: ClientServiceSubscription) -> bool:
    return (
        not bool(getattr(row, "is_locked", False))
        and (getattr(row, "status", "") or "").lower() not in {"completed", "cancelled", "inactive"}
    )


def _engagement_is_effective(row: ClientServiceSubscription, effective_from: date | None) -> bool:
    if not effective_from:
        return True
    comparison_date = (
        getattr(row, "start_date", None)
        or getattr(row, "current_due_date", None)
        or getattr(row, "original_due_date", None)
    )
    return comparison_date is None or comparison_date >= effective_from


@router.get("")
def subscription_master_list(request: Request, q: str = "", include_inactive: bool = False):
    db = CommonSessionLocal()
    try:
        user = get_current_user(request, db=db)
        if not user:
            return RedirectResponse("/login", 303)
        try:
            require_permission(db, user, "clients.view")
        except Exception:
            return _denied()

        tenant_id = _tenant_id(request, user)
        branch_id = _branch_id(request, user)
        counts = (
            select(
                ClientServiceSubscription.service_plan_id,
                func.count(ClientServiceSubscription.id).label("engagement_count"),
                func.max(ClientServiceSubscription.current_due_date).label("latest_due_date"),
            )
            .where(ClientServiceSubscription.service_plan_id.is_not(None))
            .group_by(ClientServiceSubscription.service_plan_id)
            .subquery()
        )
        stmt = (
            select(ClientServicePlan, counts.c.engagement_count, counts.c.latest_due_date)
            .options(
                selectinload(ClientServicePlan.client),
                selectinload(ClientServicePlan.catalogue),
                selectinload(ClientServicePlan.default_partner),
                selectinload(ClientServicePlan.default_performing_partner),
                selectinload(ClientServicePlan.default_manager),
                selectinload(ClientServicePlan.default_staff),
                selectinload(ClientServicePlan.default_review_partner),
            )
            .outerjoin(counts, counts.c.service_plan_id == ClientServicePlan.id)
            .where(ClientServicePlan.tenant_id == tenant_id)
        )
        if branch_id:
            stmt = stmt.where(ClientServicePlan.branch_id == branch_id)
        if not include_inactive:
            stmt = stmt.where(ClientServicePlan.is_active.is_(True))
        if q.strip():
            from app.modules.clients.models import Client
            from app.modules.services.models import ServiceCatalogue
            term = f"%{q.strip()}%"
            stmt = (
                stmt.join(Client, Client.id == ClientServicePlan.client_id)
                .join(ServiceCatalogue, ServiceCatalogue.id == ClientServicePlan.service_catalogue_id)
                .where(
                    (Client.client_name.ilike(term))
                    | (Client.client_code.ilike(term))
                    | (ServiceCatalogue.service_name.ilike(term))
                    | (ServiceCatalogue.service_code.ilike(term))
                )
            )
        rows = db.execute(
            stmt.order_by(ClientServicePlan.is_active.desc(), ClientServicePlan.id.desc())
        ).all()
        return templates.TemplateResponse(
            "modules/services/templates/services/subscriptions/list.html",
            _ctx(
                request,
                db,
                user,
                title="Client Service Subscriptions",
                rows=rows,
                scope_displays={},
                q=q,
                include_inactive=include_inactive,
                can_edit_subscription="clients.edit" in set(get_user_permissions(db, user.id)),
            ),
        )
    finally:
        db.close()



@router.get("/bulk")
def subscription_bulk_page(
    request: Request,
    error: str = "",
    subscriptions_created: int = 0,
    subscriptions_reused: int = 0,
    engagements_created: int = 0,
    engagements_skipped: int = 0,
):
    db = CommonSessionLocal()
    try:
        user = get_current_user(request, db=db)
        if not user:
            return RedirectResponse("/login", 303)
        try:
            require_permission(db, user, "clients.edit")
        except Exception:
            return _denied()

        tenant_id = _tenant_id(request, user)
        branch_id = _branch_id(request, user)
        enabled_services = list_enabled_services_for_assignment(db, tenant_id=tenant_id)
        partners = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id, role_names=("Partner",))
        managers = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id, role_names=("Branch Manager",))
        staff_users = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id, role_names=("Staff",))
        review_partners = list_review_partners(db, tenant_id=tenant_id, branch_id=branch_id)
        error_messages = {
            "targets": "Select at least one permitted Client, Business Unit, Client Branch or Registration.",
            "service": "Select a valid enabled firm service.",
            "partner": "Select a valid Engagement Partner.",
            "performing_partner": "Select a valid Performing Partner.",
            "manager": "Select a valid Manager.",
            "staff": "Select a valid Staff member.",
            "review_partner": "Select a valid Review Partner.",
            "review_partner_required": "Review Partner is mandatory for this assurance service.",
            "review_partner_independence": "Review Partner must differ from the Engagement and Performing Partners.",
            "period": "Select a valid month or quarter.",
            "generation": "Select a valid engagement generation option.",
            "financial_year": "The selected financial year is locked.",
        }
        return templates.TemplateResponse(
            "modules/services/templates/services/subscriptions/bulk.html",
            _ctx(
                request, db, user, title="Bulk Client Subscriptions",
                enabled_services=enabled_services, partners=partners, managers=managers,
                staff_users=staff_users, review_partners=review_partners,
                financial_year=_active_financial_year(request),
                error_message=error_messages.get(error, ""),
                subscriptions_created=subscriptions_created,
                subscriptions_reused=subscriptions_reused,
                engagements_created=engagements_created,
                engagements_skipped=engagements_skipped,
            ),
        )
    finally:
        db.close()


@router.get("/bulk/targets")
def subscription_bulk_targets(request: Request, service_catalogue_id: int):
    db = CommonSessionLocal()
    try:
        user = get_current_user(request, db=db)
        if not user:
            return JSONResponse({"error": "Authentication required."}, status_code=401)
        try:
            require_permission(db, user, "clients.edit")
        except Exception:
            return JSONResponse({"error": "Access denied."}, status_code=403)

        tenant_id = _tenant_id(request, user)
        branch_id = _branch_id(request, user)
        firm_selection = get_enabled_firm_service(db, tenant_id=tenant_id, service_catalogue_id=service_catalogue_id)
        if not firm_selection:
            return JSONResponse({"error": "Enabled firm service not found."}, status_code=404)

        clients = list_clients_for_assignment(
            db, tenant_id=tenant_id, branch_id=branch_id, partner_id=_partner_scope_id(db, user)
        )
        targets = list_scope_targets(db, tenant_id=tenant_id, clients=clients, catalogue=firm_selection.catalogue)
        scope_type = getattr(firm_selection.catalogue, "service_scope_type", "client") or "client"
        registration_type = getattr(firm_selection.catalogue, "required_registration_type", None)
        return {
            "scope_type": scope_type,
            "registration_type": registration_type,
            "targets": [
                {
                    "token": row.token,
                    "client_code": row.client_code,
                    "client_name": row.client_name,
                    "pan": row.pan,
                    "business_unit": row.business_unit,
                    "client_branch": row.client_branch,
                    "registration_type": row.registration_type,
                    "registration_number": row.registration_number,
                    "trade_name": row.trade_name,
                    "state": row.state,
                    "entity_type": row.entity_type,
                    "subscription_status": row.existing_plan_status or "not_subscribed",
                }
                for row in targets
            ],
        }
    finally:
        db.close()


@router.post("/bulk")
def subscription_bulk_submit(
    request: Request,
    scope_targets: list[str] = Form([]),
    service_catalogue_id: int = Form(...),
    default_partner_user_id: int = Form(...),
    default_performing_partner_user_id: str = Form(""),
    default_manager_user_id: str = Form(""),
    default_staff_user_id: str = Form(""),
    default_review_partner_user_id: str = Form(""),
    effective_from: str = Form(""),
    effective_to: str = Form(""),
    auto_generate_periods: str | None = Form(None),
    remarks: str = Form(""),
    generation_mode: str = Form("subscription_only"),
    financial_year: str = Form(""),
    period_label: str = Form(""),
    csrf_token: str = Form(...),
):
    validate_csrf(request, csrf_token)
    db = CommonSessionLocal()
    subscriptions_created = subscriptions_reused = engagements_created = engagements_skipped = 0
    try:
        user = get_current_user(request, db=db)
        if not user:
            return RedirectResponse("/login", 303)
        try:
            require_permission(db, user, "clients.edit")
        except Exception:
            return _denied()

        tenant_id = _tenant_id(request, user)
        branch_id = _branch_id(request, user)
        firm_selection = get_enabled_firm_service(db, tenant_id=tenant_id, service_catalogue_id=service_catalogue_id)
        if not firm_selection:
            return RedirectResponse("/services/subscriptions/bulk?error=service", 303)

        permitted_clients = list_clients_for_assignment(
            db, tenant_id=tenant_id, branch_id=branch_id, partner_id=_partner_scope_id(db, user)
        )
        clients_by_id = {int(row.id): row for row in permitted_clients}
        selected_tokens = list(dict.fromkeys(value for value in scope_targets if value))
        if not selected_tokens:
            return RedirectResponse("/services/subscriptions/bulk?error=targets", 303)

        plan_branch_id = branch_id or getattr(firm_selection, "default_branch_id", None) or getattr(user, "branch_id", None)
        partners = list_assignable_users(db, tenant_id=tenant_id, branch_id=plan_branch_id, role_names=("Partner",))
        managers = list_assignable_users(db, tenant_id=tenant_id, branch_id=plan_branch_id, role_names=("Branch Manager",))
        staff_users = list_assignable_users(db, tenant_id=tenant_id, branch_id=plan_branch_id, role_names=("Staff",))
        review_partners = list_review_partners(db, tenant_id=tenant_id, branch_id=plan_branch_id)

        if default_partner_user_id not in {row.id for row in partners}:
            return RedirectResponse("/services/subscriptions/bulk?error=partner", 303)
        performing_partner_id = _optional_int(default_performing_partner_user_id) or default_partner_user_id
        if performing_partner_id not in {row.id for row in partners}:
            return RedirectResponse("/services/subscriptions/bulk?error=performing_partner", 303)
        manager_id = _optional_int(default_manager_user_id)
        if manager_id is not None and manager_id not in {row.id for row in managers}:
            return RedirectResponse("/services/subscriptions/bulk?error=manager", 303)
        staff_id = _optional_int(default_staff_user_id)
        if staff_id is not None and staff_id not in {row.id for row in staff_users}:
            return RedirectResponse("/services/subscriptions/bulk?error=staff", 303)
        review_partner_id = _optional_int(default_review_partner_user_id)
        if review_partner_id is not None and review_partner_id not in {row.id for row in review_partners}:
            return RedirectResponse("/services/subscriptions/bulk?error=review_partner", 303)

        engagement_type = getattr(firm_selection.catalogue, "engagement_type", None) or "non_assurance"
        review_required = review_partner_required_for_engagement(db, tenant_id=tenant_id, engagement_type=engagement_type)
        if review_required and review_partner_id is None:
            return RedirectResponse("/services/subscriptions/bulk?error=review_partner_required", 303)
        if review_required and review_partner_id in {default_partner_user_id, performing_partner_id}:
            return RedirectResponse("/services/subscriptions/bulk?error=review_partner_independence", 303)

        if generation_mode not in {"subscription_only", "current_period", "all_periods"}:
            return RedirectResponse("/services/subscriptions/bulk?error=generation", 303)
        selected_financial_year = normalize_financial_year(financial_year or _active_financial_year(request))
        if generation_mode != "subscription_only":
            locked = redirect_if_financial_year_locked(
                db, tenant_id=tenant_id, year_code=selected_financial_year,
                redirect_url="/services/subscriptions/bulk?error=financial_year",
            )
            if locked:
                return locked

        recurrence_type = getattr(firm_selection.catalogue, "recurrence_type", None)
        if generation_mode == "subscription_only":
            requested_periods = []
        elif recurrence_requires_period(recurrence_type):
            if generation_mode == "all_periods":
                requested_periods = [code for code, _ in period_choices_for_service(selected_financial_year, recurrence_type)]
            else:
                try:
                    requested_periods = [normalize_period_label(
                        period_label, financial_year=selected_financial_year, recurrence_type=recurrence_type
                    )]
                except ValueError:
                    return RedirectResponse("/services/subscriptions/bulk?error=period", 303)
        else:
            requested_periods = [""]

        plan_effective_from = parse_date(effective_from)
        plan_effective_to = parse_date(effective_to)
        if plan_effective_from and plan_effective_to and plan_effective_to < plan_effective_from:
            return RedirectResponse("/services/subscriptions/bulk?error=period", 303)

        if (
            generation_mode == "subscription_only"
            and auto_generate_periods is not None
            and recurrence_type == "monthly"
            and plan_effective_from is not None
        ):
            if plan_effective_from.month == 1:
                initial_monthly_period = f"{plan_effective_from.year - 1:04d}-12"
            else:
                initial_monthly_period = (
                    f"{plan_effective_from.year:04d}-{plan_effective_from.month - 1:02d}"
                )
            try:
                requested_periods = [normalize_period_label(
                    initial_monthly_period,
                    financial_year=selected_financial_year,
                    recurrence_type=recurrence_type,
                )]
            except ValueError:
                return RedirectResponse("/services/subscriptions/bulk?error=period", 303)

            locked = redirect_if_financial_year_locked(
                db,
                tenant_id=tenant_id,
                year_code=selected_financial_year,
                redirect_url="/services/subscriptions/bulk?error=financial_year",
            )
            if locked:
                return locked
        for token in selected_tokens:
            try:
                client, scope_type, scope_key, business_unit_id, client_branch_id, registration_id = resolve_scope_target(
                    db, tenant_id=tenant_id, clients_by_id=clients_by_id, token=token, actor_user_id=user.id
                )
            except ValueError:
                db.rollback()
                return RedirectResponse("/services/subscriptions/bulk?error=targets", 303)

            plan = db.execute(select(ClientServicePlan).where(
                ClientServicePlan.tenant_id == tenant_id,
                ClientServicePlan.service_catalogue_id == service_catalogue_id,
                ClientServicePlan.scope_key == scope_key,
            )).scalar_one_or_none()

            if plan:
                subscriptions_reused += 1
            else:
                plan = ClientServicePlan(
                    tenant_id=tenant_id, branch_id=plan_branch_id or client.branch_id,
                    client_id=client.id, scope_type=scope_type, scope_key=scope_key,
                    business_unit_id=business_unit_id, client_branch_id=client_branch_id,
                    registration_id=registration_id, service_catalogue_id=service_catalogue_id,
                    firm_service_selection_id=firm_selection.id,
                    default_partner_user_id=default_partner_user_id,
                    default_performing_partner_user_id=performing_partner_id,
                    default_manager_user_id=manager_id, default_staff_user_id=staff_id,
                    default_review_partner_user_id=review_partner_id,
                    recurrence_type=(recurrence_type or "one_time"),
                    effective_from=plan_effective_from, effective_to=plan_effective_to,
                    auto_generate_periods=auto_generate_periods is not None,
                    status="active", is_active=True, remarks=remarks.strip() or None,
                    created_by_user_id=user.id, updated_by_user_id=user.id,
                )
                db.add(plan); db.flush()
                subscriptions_created += 1

            for requested_period in requested_periods:
                existing = db.execute(select(ClientServiceSubscription).where(
                    ClientServiceSubscription.tenant_id == tenant_id,
                    ClientServiceSubscription.service_catalogue_id == service_catalogue_id,
                    ClientServiceSubscription.scope_key == scope_key,
                    ClientServiceSubscription.financial_year == selected_financial_year,
                    ClientServiceSubscription.period_label == requested_period,
                )).scalar_one_or_none()
                if existing:
                    if existing.service_plan_id is None:
                        existing.service_plan_id = plan.id
                    engagements_skipped += 1
                    continue

                engagement = ClientServiceSubscription(
                    tenant_id=tenant_id, branch_id=plan.branch_id, service_plan_id=plan.id,
                    client_id=client.id, scope_type=scope_type, scope_key=scope_key,
                    business_unit_id=business_unit_id, client_branch_id=client_branch_id,
                    registration_id=registration_id, service_catalogue_id=service_catalogue_id,
                    firm_service_selection_id=firm_selection.id,
                    assigned_partner_user_id=default_partner_user_id,
                    performing_partner_user_id=performing_partner_id,
                    assigned_manager_user_id=manager_id, assigned_staff_user_id=staff_id,
                    review_partner_user_id=review_partner_id if review_required else None,
                    financial_year=selected_financial_year, period_label=requested_period,
                    assessment_year=assessment_year_from_financial_year(selected_financial_year),
                    engagement_type=engagement_type, start_date=plan_effective_from,
                    end_date=plan_effective_to, status="active", remarks=remarks.strip() or None,
                    is_active=True, created_by_user_id=user.id, updated_by_user_id=user.id,
                )
                db.add(engagement); db.flush()
                attach_engagement_to_plan(
                    db, engagement=engagement, client=client, catalogue=firm_selection.catalogue,
                    firm_selection=firm_selection, actor_user_id=user.id,
                )
                apply_due_date_rule_to_subscription(db, engagement, force=True)
                ensure_engagement_quality_workflow(db, subscription=engagement, actor_user_id=user.id, create_declarations=False)
                enforce_quality_gate_on_subscription(engagement)
                generate_tasks_for_subscription_if_ready(
                    db,
                    subscription=engagement,
                    user_id=user.id,
                )
                engagements_created += 1

        db.commit()
        return RedirectResponse(
            "/services/subscriptions/bulk"
            f"?subscriptions_created={subscriptions_created}&subscriptions_reused={subscriptions_reused}"
            f"&engagements_created={engagements_created}&engagements_skipped={engagements_skipped}",
            303,
        )
    except Exception:
        db.rollback()
        raise
    finally:
        db.close()



@router.get("/{plan_id}/scope")
def subscription_scope_edit_page(
    request: Request,
    plan_id: int,
    saved: int = 0,
    updated_engagements: int = 0,
    error: str = "",
):
    db = CommonSessionLocal()
    try:
        user = get_current_user(request, db=db)
        if not user:
            return RedirectResponse("/login", 303)
        try:
            require_permission(db, user, "clients.edit")
        except Exception:
            return _denied()

        tenant_id = _tenant_id(request, user)
        branch_id = _branch_id(request, user)
        plan = _load_plan(
            db,
            plan_id=plan_id,
            tenant_id=tenant_id,
            branch_id=branch_id,
        )
        if not plan:
            return RedirectResponse("/services/subscriptions", 303)

        scope_context = _registration_scope_context(
            db,
            tenant_id=tenant_id,
            plan=plan,
        )
        error_messages = {
            "scope_type": "Select a valid subscription scope.",
            "business_unit": "Select a valid Business Unit belonging to this client.",
            "client_branch": "Select a valid Client Branch belonging to this client.",
            "registration": "Select a valid active registration belonging to this client.",
            "registration_type": "The selected registration type does not match the Service Catalogue requirement.",
            "duplicate_plan": "Another subscription already exists for this service and selected scope.",
            "duplicate_engagement": "An engagement already exists for the selected scope, financial year and period.",
            "propagation": "Select a valid propagation option.",
        }
        return templates.TemplateResponse(
            "modules/services/templates/services/subscriptions/scope.html",
            _ctx(
                request,
                db,
                user,
                title="Map Subscription Scope",
                plan=plan,
                saved=saved,
                updated_engagements=updated_engagements,
                error_message=error_messages.get(error, ""),
                **scope_context,
            ),
        )
    finally:
        db.close()


@router.post("/{plan_id}/scope")
def subscription_scope_edit_submit(
    request: Request,
    plan_id: int,
    scope_type: str = Form(...),
    business_unit_id: str = Form(""),
    client_branch_id: str = Form(""),
    registration_id: str = Form(""),
    propagation_mode: str = Form("open_engagements"),
    scope_change_reason: str = Form(""),
    csrf_token: str = Form(...),
):
    validate_csrf(request, csrf_token)
    db = CommonSessionLocal()
    try:
        user = get_current_user(request, db=db)
        if not user:
            return RedirectResponse("/login", 303)
        try:
            require_permission(db, user, "clients.edit")
        except Exception:
            return _denied()

        tenant_id = _tenant_id(request, user)
        branch_id = _branch_id(request, user)
        plan = _load_plan(
            db,
            plan_id=plan_id,
            tenant_id=tenant_id,
            branch_id=branch_id,
        )
        if not plan:
            return RedirectResponse("/services/subscriptions", 303)

        normalized_scope = (
            scope_type or ""
        ).strip().lower().replace("-", "_").replace(" ", "_")
        if normalized_scope not in {
            "client",
            "business_unit",
            "client_branch",
            "registration",
        }:
            return RedirectResponse(
                f"/services/subscriptions/{plan.id}/scope?error=scope_type",
                303,
            )
        if propagation_mode not in {"subscription_only", "open_engagements"}:
            return RedirectResponse(
                f"/services/subscriptions/{plan.id}/scope?error=propagation",
                303,
            )

        selected_business_id = _optional_int(business_unit_id)
        selected_branch_id = _optional_int(client_branch_id)
        selected_registration_id = _optional_int(registration_id)

        business = None
        client_branch = None
        registration = None
        registration_type = None

        if selected_business_id:
            business = db.get(ClientBusinessUnit, selected_business_id)
            if (
                not business
                or business.tenant_id != tenant_id
                or business.client_id != plan.client_id
                or not business.is_active
            ):
                return RedirectResponse(
                    f"/services/subscriptions/{plan.id}/scope?error=business_unit",
                    303,
                )

        if selected_branch_id:
            client_branch = db.get(ClientBranch, selected_branch_id)
            if (
                not client_branch
                or client_branch.tenant_id != tenant_id
                or client_branch.client_id != plan.client_id
                or not client_branch.is_active
            ):
                return RedirectResponse(
                    f"/services/subscriptions/{plan.id}/scope?error=client_branch",
                    303,
                )
            selected_business_id = client_branch.business_unit_id
            business = db.get(ClientBusinessUnit, selected_business_id)

        if selected_registration_id:
            registration = db.get(ClientRegistration, selected_registration_id)
            if (
                not registration
                or registration.tenant_id != tenant_id
                or registration.client_id != plan.client_id
                or registration.status not in {"active", "valid", "registered"}
            ):
                return RedirectResponse(
                    f"/services/subscriptions/{plan.id}/scope?error=registration",
                    303,
                )
            registration_type = db.get(
                RegistrationType,
                registration.registration_type_id,
            )
            selected_business_id = (
                registration.business_unit_id or selected_business_id
            )
            selected_branch_id = (
                registration.client_branch_id or selected_branch_id
            )
            if selected_business_id:
                business = db.get(ClientBusinessUnit, selected_business_id)
            if selected_branch_id:
                client_branch = db.get(ClientBranch, selected_branch_id)

        if normalized_scope == "client":
            target_id = plan.client_id
            selected_business_id = None
            selected_branch_id = None
            selected_registration_id = None
        elif normalized_scope == "business_unit":
            if not business:
                return RedirectResponse(
                    f"/services/subscriptions/{plan.id}/scope?error=business_unit",
                    303,
                )
            target_id = business.id
            selected_branch_id = None
            selected_registration_id = None
        elif normalized_scope == "client_branch":
            if not client_branch:
                return RedirectResponse(
                    f"/services/subscriptions/{plan.id}/scope?error=client_branch",
                    303,
                )
            target_id = client_branch.id
            selected_registration_id = None
        else:
            if not registration or not registration_type:
                return RedirectResponse(
                    f"/services/subscriptions/{plan.id}/scope?error=registration",
                    303,
                )
            required_type = (
                getattr(plan.catalogue, "required_registration_type", None)
                or ""
            ).strip().upper()
            actual_type = (registration_type.code or "").strip().upper()
            if required_type and required_type != actual_type:
                return RedirectResponse(
                    f"/services/subscriptions/{plan.id}/scope?error=registration_type",
                    303,
                )
            target_id = registration.id

        new_scope_key = _scope_key_for(normalized_scope, target_id)

        conflicting_plan = db.execute(
            select(ClientServicePlan).where(
                ClientServicePlan.tenant_id == tenant_id,
                ClientServicePlan.service_catalogue_id
                == plan.service_catalogue_id,
                ClientServicePlan.scope_key == new_scope_key,
                ClientServicePlan.id != plan.id,
            )
        ).scalar_one_or_none()
        if conflicting_plan:
            return RedirectResponse(
                f"/services/subscriptions/{plan.id}/scope?error=duplicate_plan",
                303,
            )

        open_engagements = []
        if propagation_mode == "open_engagements":
            open_engagements = db.execute(
                select(ClientServiceSubscription).where(
                    ClientServiceSubscription.tenant_id == tenant_id,
                    ClientServiceSubscription.service_plan_id == plan.id,
                )
            ).scalars().all()
            open_engagements = [
                row for row in open_engagements if _engagement_is_open(row)
            ]

            for row in open_engagements:
                conflict = db.execute(
                    select(ClientServiceSubscription.id).where(
                        ClientServiceSubscription.tenant_id == tenant_id,
                        ClientServiceSubscription.service_catalogue_id
                        == plan.service_catalogue_id,
                        ClientServiceSubscription.scope_key == new_scope_key,
                        ClientServiceSubscription.financial_year
                        == row.financial_year,
                        ClientServiceSubscription.period_label
                        == row.period_label,
                        ClientServiceSubscription.id != row.id,
                    )
                ).scalar_one_or_none()
                if conflict:
                    return RedirectResponse(
                        f"/services/subscriptions/{plan.id}/scope?error=duplicate_engagement",
                        303,
                    )

        old_scope = plan.scope_key or f"CLIENT:{plan.client_id}"
        plan.scope_type = normalized_scope
        plan.scope_key = new_scope_key
        plan.business_unit_id = selected_business_id
        plan.client_branch_id = selected_branch_id
        plan.registration_id = selected_registration_id
        plan.updated_by_user_id = user.id
        plan.updated_at_utc = datetime.now(timezone.utc)

        updated_engagements = 0
        if propagation_mode == "open_engagements":
            for row in open_engagements:
                row.scope_type = normalized_scope
                row.scope_key = new_scope_key
                row.business_unit_id = selected_business_id
                row.client_branch_id = selected_branch_id
                row.registration_id = selected_registration_id
                row.updated_by_user_id = user.id
                row.updated_at_utc = datetime.now(timezone.utc)
                if scope_change_reason.strip():
                    note = (
                        f"Subscription scope changed from {old_scope} "
                        f"to {new_scope_key} on "
                        f"{datetime.now(timezone.utc).date().isoformat()} "
                        f"by user {user.id}: "
                        f"{scope_change_reason.strip()}"
                    )
                    row.remarks = (
                        f"{row.remarks.strip()}\n{note}"
                        if row.remarks
                        else note
                    )
                updated_engagements += 1

        db.commit()
        return RedirectResponse(
            f"/services/subscriptions/{plan.id}/scope?saved=1"
            f"&updated_engagements={updated_engagements}",
            303,
        )
    except Exception:
        db.rollback()
        raise
    finally:
        db.close()

@router.get("/{plan_id}/edit")
def subscription_master_edit_page(
    request: Request,
    plan_id: int,
    saved: int = 0,
    updated_engagements: int = 0,
    updated_tasks: int = 0,
    error: str = "",
):
    db = CommonSessionLocal()
    try:
        user = get_current_user(request, db=db)
        if not user:
            return RedirectResponse("/login", 303)
        try:
            require_permission(db, user, "clients.edit")
        except Exception:
            return _denied()

        tenant_id = _tenant_id(request, user)
        branch_id = _branch_id(request, user)
        plan = _load_plan(db, plan_id=plan_id, tenant_id=tenant_id, branch_id=branch_id)
        if not plan:
            return RedirectResponse("/services/subscriptions", 303)

        partners = list_assignable_users(
            db, tenant_id=tenant_id, branch_id=plan.branch_id, role_names=("Partner",)
        )
        managers = list_assignable_users(
            db, tenant_id=tenant_id, branch_id=plan.branch_id, role_names=("Branch Manager",)
        )
        staff_users = list_assignable_users(
            db, tenant_id=tenant_id, branch_id=plan.branch_id, role_names=("Staff",)
        )
        review_partners = list_review_partners(
            db, tenant_id=tenant_id, branch_id=plan.branch_id
        )
        return templates.TemplateResponse(
            "modules/services/templates/services/subscriptions/edit.html",
            _ctx(
                request,
                db,
                user,
                title="Edit Client Service Subscription",
                plan=plan,
                partners=partners,
                managers=managers,
                staff_users=staff_users,
                review_partners=review_partners,
                saved=saved,
                updated_engagements=updated_engagements,
                updated_tasks=updated_tasks,
                error=error,
            ),
        )
    finally:
        db.close()


@router.post("/{plan_id}/edit")
def subscription_master_edit_submit(
    request: Request,
    plan_id: int,
    default_partner_user_id: str = Form(""),
    default_performing_partner_user_id: str = Form(""),
    default_manager_user_id: str = Form(""),
    default_staff_user_id: str = Form(""),
    default_review_partner_user_id: str = Form(""),
    effective_from: str = Form(""),
    effective_to: str = Form(""),
    status: str = Form("active"),
    auto_generate_periods: str | None = Form(None),
    remarks: str = Form(""),
    propagation_mode: str = Form("future_only"),
    reassignment_effective_from: str = Form(""),
    reassignment_reason: str = Form(""),
    csrf_token: str = Form(...),
):
    validate_csrf(request, csrf_token)
    db = CommonSessionLocal()
    try:
        user = get_current_user(request, db=db)
        if not user:
            return RedirectResponse("/login", 303)
        try:
            require_permission(db, user, "clients.edit")
        except Exception:
            return _denied()

        tenant_id = _tenant_id(request, user)
        branch_id = _branch_id(request, user)
        plan = _load_plan(db, plan_id=plan_id, tenant_id=tenant_id, branch_id=branch_id)
        if not plan:
            return RedirectResponse("/services/subscriptions", 303)

        partners = list_assignable_users(
            db, tenant_id=tenant_id, branch_id=plan.branch_id, role_names=("Partner",)
        )
        managers = list_assignable_users(
            db, tenant_id=tenant_id, branch_id=plan.branch_id, role_names=("Branch Manager",)
        )
        staff_users = list_assignable_users(
            db, tenant_id=tenant_id, branch_id=plan.branch_id, role_names=("Staff",)
        )
        review_partners = list_review_partners(
            db, tenant_id=tenant_id, branch_id=plan.branch_id
        )

        try:
            new_partner_id = _allowed_id(
                _optional_int(default_partner_user_id), {row.id for row in partners}, "partner"
            )
            requested_performing_id = _allowed_id(
                _optional_int(default_performing_partner_user_id),
                {row.id for row in partners},
                "performing_partner",
            )
            new_performing_id = requested_performing_id or new_partner_id
            new_manager_id = _allowed_id(
                _optional_int(default_manager_user_id), {row.id for row in managers}, "manager"
            )
            new_staff_id = _allowed_id(
                _optional_int(default_staff_user_id), {row.id for row in staff_users}, "staff"
            )
            new_review_partner_id = _allowed_id(
                _optional_int(default_review_partner_user_id),
                {row.id for row in review_partners},
                "review_partner",
            )
            plan_effective_from = parse_date(effective_from)
            plan_effective_to = parse_date(effective_to)
            change_effective_from = parse_date(reassignment_effective_from)
        except (TypeError, ValueError) as exc:
            code = str(exc) if str(exc) else "validation"
            db.rollback()
            return RedirectResponse(
                f"/services/subscriptions/{plan.id}/edit?error={code}", status_code=303
            )

        allowed_statuses = {item[0] for item in SUBSCRIPTION_STATUSES}
        normalized_status = (status or "active").strip().lower()
        if normalized_status not in allowed_statuses:
            return RedirectResponse(
                f"/services/subscriptions/{plan.id}/edit?error=status", status_code=303
            )
        if plan_effective_from and plan_effective_to and plan_effective_to < plan_effective_from:
            return RedirectResponse(
                f"/services/subscriptions/{plan.id}/edit?error=dates", status_code=303
            )
        if propagation_mode not in {"future_only", "open_engagements"}:
            return RedirectResponse(
                f"/services/subscriptions/{plan.id}/edit?error=propagation", status_code=303
            )

        old_staff_id = plan.default_staff_user_id
        plan.default_partner_user_id = new_partner_id
        plan.default_performing_partner_user_id = new_performing_id
        plan.default_manager_user_id = new_manager_id
        plan.default_staff_user_id = new_staff_id
        plan.default_review_partner_user_id = new_review_partner_id
        plan.effective_from = plan_effective_from
        plan.effective_to = plan_effective_to
        plan.auto_generate_periods = auto_generate_periods is not None
        plan.status = normalized_status
        plan.is_active = normalized_status not in {"inactive", "cancelled"}
        plan.remarks = remarks.strip() or None
        plan.updated_by_user_id = user.id
        plan.updated_at_utc = datetime.now(timezone.utc)

        updated_engagement_count = 0
        updated_task_count = 0
        if propagation_mode == "open_engagements":
            engagements = db.execute(
                select(ClientServiceSubscription).where(
                    ClientServiceSubscription.service_plan_id == plan.id,
                    ClientServiceSubscription.tenant_id == tenant_id,
                )
            ).scalars().all()

            affected_ids: list[int] = []
            for row in engagements:
                if not _engagement_is_open(row):
                    continue
                if not _engagement_is_effective(row, change_effective_from):
                    continue
                row.assigned_partner_user_id = new_partner_id
                row.performing_partner_user_id = new_performing_id
                row.assigned_manager_user_id = new_manager_id
                row.assigned_staff_user_id = new_staff_id
                row.review_partner_user_id = new_review_partner_id
                row.updated_by_user_id = user.id
                row.updated_at_utc = datetime.now(timezone.utc)
                if reassignment_reason.strip():
                    note = (
                        f"Subscription team updated on {datetime.now(timezone.utc).date().isoformat()} "
                        f"by user {user.id}: {reassignment_reason.strip()}"
                    )
                    row.remarks = f"{row.remarks.strip()}\n{note}" if row.remarks else note
                affected_ids.append(row.id)
                updated_engagement_count += 1

            if affected_ids and old_staff_id != new_staff_id:
                task_stmt = select(ClientServiceTaskInstance).where(
                    ClientServiceTaskInstance.subscription_id.in_(affected_ids),
                    ClientServiceTaskInstance.is_active.is_(True),
                    ClientServiceTaskInstance.is_locked.is_(False),
                    ClientServiceTaskInstance.status.notin_(("completed", "cancelled")),
                )
                if old_staff_id is None:
                    task_stmt = task_stmt.where(
                        ClientServiceTaskInstance.assigned_to_user_id.is_(None),
                        func.lower(func.coalesce(ClientServiceTaskInstance.default_role_name, "")) == "staff",
                    )
                else:
                    task_stmt = task_stmt.where(
                        ClientServiceTaskInstance.assigned_to_user_id == old_staff_id
                    )
                tasks = db.execute(task_stmt).scalars().all()
                for task in tasks:
                    task.assigned_to_user_id = new_staff_id
                    task.updated_by_user_id = user.id
                    task.updated_at_utc = datetime.now(timezone.utc)
                    updated_task_count += 1

        db.commit()
        return RedirectResponse(
            f"/services/subscriptions/{plan.id}/edit?saved=1"
            f"&updated_engagements={updated_engagement_count}"
            f"&updated_tasks={updated_task_count}",
            status_code=303,
        )
    except Exception:
        db.rollback()
        raise
    finally:
        db.close()


@router.get("/{plan_id}")
def subscription_master_detail(request: Request, plan_id: int):
    db = CommonSessionLocal()
    try:
        user = get_current_user(request, db=db)
        if not user:
            return RedirectResponse("/login", 303)
        try:
            require_permission(db, user, "clients.view")
        except Exception:
            return _denied()
        tenant_id = _tenant_id(request, user)
        branch_id = _branch_id(request, user)
        plan = _load_plan(db, plan_id=plan_id, tenant_id=tenant_id, branch_id=branch_id)
        if not plan:
            return RedirectResponse("/services/subscriptions", 303)
        engagements = db.execute(
            select(ClientServiceSubscription)
            .where(ClientServiceSubscription.service_plan_id == plan.id)
            .order_by(
                ClientServiceSubscription.financial_year.desc(),
                ClientServiceSubscription.period_label.asc(),
                ClientServiceSubscription.id.desc(),
            )
        ).scalars().all()
        permissions = set(get_user_permissions(db, user.id))
        return templates.TemplateResponse(
            "modules/services/templates/services/subscriptions/detail.html",
            _ctx(
                request,
                db,
                user,
                title="Client Service Subscription",
                plan=plan,
                engagements=engagements,
                can_edit_subscription="clients.edit" in permissions,
                **_registration_scope_context(
                    db,
                    tenant_id=tenant_id,
                    plan=plan,
                ),
            ),
        )
    finally:
        db.close()
