from __future__ import annotations

from collections import OrderedDict
from datetime import date, datetime, timedelta, timezone
from typing import Any

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

from app.modules.clients.models import Client
from app.modules.consultants.models import ClientConsultantLink, ConsultantProfile, ConsultantServiceRequest
from app.modules.documents.models import EngagementDocument, PermanentClientDocument
from app.modules.services.models import (
    ClientServiceSubscription,
    ClientServiceTaskInstance,
    ServiceCatalogue,
    ServiceTaskComment,
    ServiceTaskDocumentRequest,
)

CONSULTANT_BOARD_COLUMNS = OrderedDict(
    [
        ("assigned", "Assigned"),
        ("awaiting_documents", "Awaiting Documents"),
        ("in_progress", "In Progress"),
        ("submitted", "Submitted"),
        ("accepted", "Accepted"),
        ("closed", "Closed"),
    ]
)


def _allowed_client_ids(db: Session, *, consultant: ConsultantProfile, require_communications: bool = False) -> list[int]:
    query = select(ClientConsultantLink.client_id).where(
        ClientConsultantLink.tenant_id == consultant.tenant_id,
        ClientConsultantLink.consultant_id == consultant.id,
        ClientConsultantLink.is_active.is_(True),
    )
    if require_communications:
        query = query.where(ClientConsultantLink.can_view_communications.is_(True))
    return [int(x) for x in db.execute(query).scalars().all()]


def _board_key_for_status(status: str | None) -> str:
    value = (status or "pending").strip().lower()
    if value in {"blocked", "awaiting_documents", "document_pending", "clarification_required"}:
        return "awaiting_documents"
    if value in {"in_progress", "under_process", "processing", "started"}:
        return "in_progress"
    if value in {"pending_review", "ready_for_review", "submitted", "completed"}:
        return "submitted"
    if value in {"approved", "accepted"}:
        return "accepted"
    if value in {"closed", "locked", "cancelled", "inactive"}:
        return "closed"
    return "assigned"


def _matches_search(*values: Any, q: str = "") -> bool:
    term = (q or "").strip().lower()
    if not term:
        return True
    return any(term in str(v or "").lower() for v in values)


TASK_PROGRESS_WEIGHTS = {
    "pending": 0,
    "assigned": 10,
    "accepted": 10,
    "blocked": 20,
    "awaiting_documents": 20,
    "document_pending": 20,
    "clarification_required": 25,
    "in_progress": 50,
    "under_process": 50,
    "processing": 50,
    "started": 50,
    "rework_required": 60,
    "pending_review": 80,
    "ready_for_review": 80,
    "submitted": 80,
    "completed": 100,
    "approved": 100,
    "accepted_final": 100,
    "closed": 100,
    "locked": 100,
}


def _task_progress(task: ClientServiceTaskInstance) -> int:
    status = (task.status or "pending").strip().lower()
    if status == "completed" and (task.manager_review_status or "").lower() in {"pending", "submitted"}:
        return 80
    if status == "completed" and (task.partner_review_status or "").lower() in {"pending", "submitted"}:
        return 90
    if (task.consultant_assignment_status or "").lower() == "rework_required":
        return 60
    return TASK_PROGRESS_WEIGHTS.get(status, 0)


def _friendly_pause_reason(code: str | None, notes: str | None) -> tuple[str, str]:
    value = (code or "").strip().lower()
    mappings = {
        "documents_required": ("Documents pending from client", "Client"),
        "clarification_needed": ("Clarification pending from client", "Client"),
        "client_asked_to_hold": ("Engagement on hold at client request", "Client"),
        "payment_pending": ("Payment or commercial confirmation pending", "Client"),
        "government_portal_issue": ("Government portal issue", "Government department"),
        "third_party_information": ("Third-party information awaited", "Third party"),
        "consultant_response_pending": ("Clarification pending from consultant", "Consultant"),
        "internal_review": ("Internal review in progress", "Firm"),
        "partner_review": ("Final review in progress", "Firm"),
    }
    text, pending_from = mappings.get(value, ("Engagement temporarily on hold", "Firm"))
    if notes and pending_from != "Firm":
        text = notes.strip()
    return text, pending_from


def _engagement_progress_payload(
    db: Session,
    *,
    subscription: ClientServiceSubscription,
    client: Client,
    catalogue: ServiceCatalogue,
    link: ClientConsultantLink,
    tasks: list[ClientServiceTaskInstance],
) -> dict:
    applicable = [task for task in tasks if task.is_active]
    percentage = round(sum(_task_progress(task) for task in applicable) / len(applicable)) if applicable else 0
    completed_count = sum(1 for task in applicable if _task_progress(task) >= 100)
    under_review = any(_task_progress(task) in {80, 90} for task in applicable)
    in_progress = any(0 < _task_progress(task) < 80 for task in applicable)

    blocker_text = "No blocker"
    pending_from = "No pending action"
    blocker_code = "none"

    if subscription.workflow_paused_at_utc and not subscription.workflow_resumed_at_utc:
        blocker_text, pending_from = _friendly_pause_reason(subscription.workflow_pause_reason, subscription.workflow_pause_notes)
        blocker_code = (subscription.workflow_pause_reason or "on_hold").lower()
    else:
        open_requests = db.execute(
            select(ServiceTaskDocumentRequest)
            .where(
                ServiceTaskDocumentRequest.tenant_id == subscription.tenant_id,
                ServiceTaskDocumentRequest.subscription_id == subscription.id,
                ServiceTaskDocumentRequest.is_active.is_(True),
                ServiceTaskDocumentRequest.status.in_(["pending", "clarification_required", "rejected"]),
            )
            .order_by(ServiceTaskDocumentRequest.due_date.asc(), ServiceTaskDocumentRequest.created_at_utc.asc())
        ).scalars().all()
        if open_requests:
            request = open_requests[0]
            recipient = (request.requested_from or "client_and_consultant").lower()
            pending_from = "Consultant" if recipient == "consultant" else "Client"
            if recipient == "client_and_consultant":
                pending_from = "Client / Consultant"
            blocker_code = "document_or_clarification_pending"
            blocker_text = request.title or "Documents or clarification awaited"
        elif subscription.quality_block_reason:
            blocker_code = "quality_clearance_pending"
            blocker_text = "Firm quality clearance in progress"
            pending_from = "Firm"
        elif any((task.consultant_assignment_status or "").lower() in {"offered", "rework_required"} for task in applicable):
            blocker_code = "consultant_action_pending"
            blocker_text = "Consultant action or rework pending"
            pending_from = "Consultant"
        elif any((task.rework_status or "").lower() == "requested" for task in applicable):
            blocker_code = "internal_rework"
            blocker_text = "Internal review and correction in progress"
            pending_from = "Firm"

    if percentage >= 100 or (subscription.status or "").lower() in {"completed", "closed", "locked"}:
        stage = "Completed"
        blocker_text = "No blocker"
        pending_from = "No pending action"
        blocker_code = "none"
    elif subscription.workflow_paused_at_utc and not subscription.workflow_resumed_at_utc:
        stage = "On Hold"
    elif blocker_code == "document_or_clarification_pending":
        stage = "Documents / Clarification Awaited"
    elif under_review:
        stage = "Under Review"
    elif percentage >= 90:
        stage = "Finalisation in Progress"
    elif in_progress or percentage > 0:
        stage = "Preparation in Progress"
    else:
        stage = "Not Started"

    due_date = subscription.current_due_date or subscription.original_due_date or subscription.expiry_date
    assigned_tasks = [
        task for task in applicable
        if task.execution_mode == "consultant" and task.assigned_consultant_id == link.consultant_id
    ]
    last_update = max(
        [subscription.updated_at_utc] + [task.updated_at_utc for task in applicable if task.updated_at_utc],
        default=subscription.updated_at_utc,
    )
    return {
        "subscription": subscription,
        "client": client,
        "catalogue": catalogue,
        "link": link,
        "progress_percentage": max(0, min(100, percentage)),
        "external_stage": stage,
        "blocker_code": blocker_code,
        "blocker_text": blocker_text,
        "pending_from": pending_from,
        "due_date": due_date,
        "is_overdue": bool(due_date and due_date < date.today() and percentage < 100),
        "total_tasks": len(applicable),
        "completed_tasks": completed_count,
        "assigned_tasks": assigned_tasks,
        "last_update_at": last_update,
    }


def _effective_link(link: ClientConsultantLink, today: date) -> bool:
    return not (link.effective_from and link.effective_from > today) and not (link.effective_to and link.effective_to < today)


def get_consultant_work_board(db: Session, *, consultant: ConsultantProfile, q: str = "", status: str = "") -> dict:
    today = date.today()
    links = db.execute(
        select(ClientConsultantLink).where(
            ClientConsultantLink.tenant_id == consultant.tenant_id,
            ClientConsultantLink.consultant_id == consultant.id,
            ClientConsultantLink.is_active.is_(True),
            ClientConsultantLink.can_view_engagements.is_(True),
            # Backward-compatible permission: this now controls engagement-level
            # progress visibility, not exposure of every internal task status.
            ClientConsultantLink.can_view_task_status.is_(True),
        )
    ).scalars().all()
    links = [link for link in links if _effective_link(link, today)]
    links_by_client: dict[int, list[ClientConsultantLink]] = {}
    for link in links:
        links_by_client.setdefault(int(link.client_id), []).append(link)

    engagements: list[dict] = []
    assigned_tasks: list[dict] = []
    if links_by_client:
        subscriptions = db.execute(
            select(ClientServiceSubscription, Client, ServiceCatalogue)
            .join(Client, Client.id == ClientServiceSubscription.client_id)
            .join(ServiceCatalogue, ServiceCatalogue.id == ClientServiceSubscription.service_catalogue_id)
            .where(
                ClientServiceSubscription.tenant_id == consultant.tenant_id,
                ClientServiceSubscription.client_id.in_(list(links_by_client)),
                ClientServiceSubscription.is_active.is_(True),
            )
            .order_by(ClientServiceSubscription.current_due_date.asc(), ClientServiceSubscription.id.desc())
            .limit(500)
        ).all()
        subscription_ids = [int(row[0].id) for row in subscriptions]
        tasks_by_subscription: dict[int, list[ClientServiceTaskInstance]] = {}
        if subscription_ids:
            all_tasks = db.execute(
                select(ClientServiceTaskInstance)
                .where(
                    ClientServiceTaskInstance.tenant_id == consultant.tenant_id,
                    ClientServiceTaskInstance.subscription_id.in_(subscription_ids),
                    ClientServiceTaskInstance.is_active.is_(True),
                )
                .order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())
            ).scalars().all()
            for task in all_tasks:
                tasks_by_subscription.setdefault(int(task.subscription_id), []).append(task)

        for subscription, client, catalogue in subscriptions:
            allowed = next(
                (
                    link for link in links_by_client[int(client.id)]
                    if link.service_catalogue_id in (None, subscription.service_catalogue_id)
                ),
                None,
            )
            if not allowed:
                continue
            item = _engagement_progress_payload(
                db,
                subscription=subscription,
                client=client,
                catalogue=catalogue,
                link=allowed,
                tasks=tasks_by_subscription.get(int(subscription.id), []),
            )
            if not _matches_search(
                client.client_name,
                getattr(client, "client_code", ""),
                catalogue.service_name,
                subscription.financial_year,
                subscription.assessment_year,
                item["external_stage"],
                item["blocker_text"],
                q=q,
            ):
                continue
            if status and (item["external_stage"] or "").lower().replace(" / ", "_").replace(" ", "_") != status:
                continue
            engagements.append(item)
            for task in item["assigned_tasks"]:
                assigned_tasks.append({"task": task, "client": client, "catalogue": catalogue, "subscription": subscription, "is_overdue": bool(task.consultant_due_date and task.consultant_due_date < today)})

    service_requests = db.execute(
        select(ConsultantServiceRequest)
        .options(selectinload(ConsultantServiceRequest.managed_client), selectinload(ConsultantServiceRequest.firm_client), selectinload(ConsultantServiceRequest.service_catalogue))
        .where(
            ConsultantServiceRequest.tenant_id == consultant.tenant_id,
            ConsultantServiceRequest.consultant_id == consultant.id,
            ConsultantServiceRequest.is_active.is_(True),
        )
        .order_by(ConsultantServiceRequest.created_at_utc.desc())
        .limit(50)
    ).scalars().all()
    stage_options = sorted({(item["external_stage"].lower().replace(" / ", "_").replace(" ", "_"), item["external_stage"]) for item in engagements})
    return {
        "engagements": engagements,
        "assigned_tasks": assigned_tasks,
        "stage_options": stage_options,
        "total_engagements": len(engagements),
        "average_progress": round(sum(item["progress_percentage"] for item in engagements) / len(engagements)) if engagements else 0,
        "blocked_engagements": sum(1 for item in engagements if item["blocker_code"] != "none"),
        "overdue_engagements": sum(1 for item in engagements if item["is_overdue"]),
        "service_requests": service_requests,
    }


def get_consultant_engagement_progress(db: Session, *, consultant: ConsultantProfile, subscription_id: int) -> dict | None:
    row = db.execute(
        select(ClientServiceSubscription, Client, ServiceCatalogue)
        .join(Client, Client.id == ClientServiceSubscription.client_id)
        .join(ServiceCatalogue, ServiceCatalogue.id == ClientServiceSubscription.service_catalogue_id)
        .where(
            ClientServiceSubscription.id == subscription_id,
            ClientServiceSubscription.tenant_id == consultant.tenant_id,
            ClientServiceSubscription.is_active.is_(True),
        )
    ).first()
    if not row:
        return None
    subscription, client, catalogue = row
    link = db.execute(
        select(ClientConsultantLink).where(
            ClientConsultantLink.tenant_id == consultant.tenant_id,
            ClientConsultantLink.client_id == client.id,
            ClientConsultantLink.consultant_id == consultant.id,
            ClientConsultantLink.is_active.is_(True),
            ClientConsultantLink.can_view_engagements.is_(True),
            ClientConsultantLink.can_view_task_status.is_(True),
            or_(ClientConsultantLink.service_catalogue_id.is_(None), ClientConsultantLink.service_catalogue_id == subscription.service_catalogue_id),
        )
    ).scalars().first()
    if not link or not _effective_link(link, date.today()):
        return None
    tasks = db.execute(
        select(ClientServiceTaskInstance)
        .where(
            ClientServiceTaskInstance.tenant_id == consultant.tenant_id,
            ClientServiceTaskInstance.subscription_id == subscription.id,
            ClientServiceTaskInstance.is_active.is_(True),
        )
        .order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())
    ).scalars().all()
    item = _engagement_progress_payload(db, subscription=subscription, client=client, catalogue=catalogue, link=link, tasks=tasks)
    communications = db.execute(
        select(ServiceTaskComment)
        .join(ClientServiceTaskInstance, ClientServiceTaskInstance.id == ServiceTaskComment.task_instance_id)
        .where(
            ServiceTaskComment.tenant_id == consultant.tenant_id,
            ClientServiceTaskInstance.subscription_id == subscription.id,
            ServiceTaskComment.visibility == "consultant",
            ServiceTaskComment.is_deleted.is_(False),
        )
        .order_by(ServiceTaskComment.created_at_utc.desc(), ServiceTaskComment.id.desc())
        .limit(25)
    ).scalars().all()
    pending_requirements = []
    if link.can_view_document_requests:
        pending_requirements = db.execute(
            select(ServiceTaskDocumentRequest)
            .where(
                ServiceTaskDocumentRequest.tenant_id == consultant.tenant_id,
                ServiceTaskDocumentRequest.subscription_id == subscription.id,
                ServiceTaskDocumentRequest.is_active.is_(True),
                ServiceTaskDocumentRequest.status.in_(["pending", "clarification_required", "rejected"]),
            )
            .order_by(ServiceTaskDocumentRequest.due_date.asc(), ServiceTaskDocumentRequest.created_at_utc.asc())
        ).scalars().all()
    item.update({"communications": communications, "pending_requirements": pending_requirements})
    return item


def get_consultant_assignment_detail(db: Session, *, consultant: ConsultantProfile, task_id: int) -> dict | None:
    row = db.execute(
        select(ClientServiceTaskInstance, ClientServiceSubscription, Client, ServiceCatalogue)
        .join(ClientServiceSubscription, ClientServiceSubscription.id == ClientServiceTaskInstance.subscription_id)
        .join(Client, Client.id == ClientServiceTaskInstance.client_id)
        .join(ServiceCatalogue, ServiceCatalogue.id == ClientServiceTaskInstance.service_catalogue_id)
        .where(
            ClientServiceTaskInstance.id == task_id,
            ClientServiceTaskInstance.tenant_id == consultant.tenant_id,
            ClientServiceTaskInstance.is_active.is_(True),
            ClientServiceTaskInstance.execution_mode == "consultant",
            ClientServiceTaskInstance.assigned_consultant_id == consultant.id,
        )
    ).first()
    if not row:
        return None
    task, subscription, client, catalogue = row
    link = db.execute(
        select(ClientConsultantLink).where(
            ClientConsultantLink.tenant_id == consultant.tenant_id,
            ClientConsultantLink.client_id == 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 == task.service_catalogue_id),
        )
    ).scalars().first()
    if not link or not _effective_link(link, date.today()):
        return None
    timeline = db.execute(
        select(ServiceTaskComment)
        .options(selectinload(ServiceTaskComment.created_by))
        .where(
            ServiceTaskComment.tenant_id == consultant.tenant_id,
            ServiceTaskComment.task_instance_id == task.id,
            ServiceTaskComment.visibility == "consultant",
            ServiceTaskComment.is_deleted.is_(False),
        )
        .order_by(ServiceTaskComment.created_at_utc.asc(), ServiceTaskComment.id.asc())
    ).scalars().all()
    engagement_documents = db.execute(
        select(EngagementDocument)
        .options(selectinload(EngagementDocument.versions))
        .where(
            EngagementDocument.tenant_id == consultant.tenant_id,
            EngagementDocument.client_id == client.id,
            EngagementDocument.engagement_id == subscription.id,
            EngagementDocument.is_deleted.is_(False),
        )
        .order_by(EngagementDocument.document_type.asc(), EngagementDocument.title.asc())
        .limit(50)
    ).scalars().all()
    if not (link.can_view_final_documents or link.can_upload_documents):
        engagement_documents = []
    permanent_documents = db.execute(
        select(PermanentClientDocument)
        .options(selectinload(PermanentClientDocument.versions))
        .where(
            PermanentClientDocument.tenant_id == consultant.tenant_id,
            PermanentClientDocument.client_id == client.id,
            PermanentClientDocument.is_deleted.is_(False),
        )
        .order_by(PermanentClientDocument.category.asc(), PermanentClientDocument.title.asc())
        .limit(50)
    ).scalars().all()
    if not link.can_view_permanent_documents:
        permanent_documents = []
    return {
        "link": link,
        "task": task,
        "subscription": subscription,
        "client": client,
        "catalogue": catalogue,
        "timeline": timeline,
        "engagement_documents": engagement_documents,
        "permanent_documents": permanent_documents,
    }


def get_consultant_document_centre(db: Session, *, consultant: ConsultantProfile, q: str = "") -> dict:
    links=db.execute(select(ClientConsultantLink).where(ClientConsultantLink.tenant_id==consultant.tenant_id,ClientConsultantLink.consultant_id==consultant.id,ClientConsultantLink.is_active.is_(True))).scalars().all()
    engagement_ids={int(x.client_id) for x in links if x.can_view_final_documents or x.can_upload_documents}
    permanent_ids={int(x.client_id) for x in links if x.can_view_permanent_documents}
    engagement_documents=[]; permanent_documents=[]
    if engagement_ids:
        stmt=select(EngagementDocument).options(selectinload(EngagementDocument.client),selectinload(EngagementDocument.engagement),selectinload(EngagementDocument.versions)).where(EngagementDocument.tenant_id==consultant.tenant_id,EngagementDocument.client_id.in_(engagement_ids),EngagementDocument.is_deleted.is_(False))
        if q.strip():
            term=f"%{q.strip()}%"; stmt=stmt.where(or_(EngagementDocument.title.ilike(term),EngagementDocument.document_type.ilike(term),EngagementDocument.document_code.ilike(term)))
        engagement_documents=db.execute(stmt.order_by(EngagementDocument.created_at_utc.desc()).limit(200)).scalars().all()
    if permanent_ids:
        stmt=select(PermanentClientDocument).options(selectinload(PermanentClientDocument.client),selectinload(PermanentClientDocument.versions)).where(PermanentClientDocument.tenant_id==consultant.tenant_id,PermanentClientDocument.client_id.in_(permanent_ids),PermanentClientDocument.is_deleted.is_(False))
        if q.strip():
            term=f"%{q.strip()}%"; stmt=stmt.where(or_(PermanentClientDocument.title.ilike(term),PermanentClientDocument.category.ilike(term),PermanentClientDocument.document_code.ilike(term)))
        permanent_documents=db.execute(stmt.order_by(PermanentClientDocument.created_at_utc.desc()).limit(200)).scalars().all()
    return {"engagement_documents":engagement_documents,"permanent_documents":permanent_documents,"total":len(engagement_documents)+len(permanent_documents)}


def consultant_can_execute_task(*, consultant: ConsultantProfile, task: ClientServiceTaskInstance) -> bool:
    return bool(task.execution_mode == "consultant" and task.assigned_consultant_id == consultant.id and task.is_active and not task.is_locked)


def update_consultant_assignment(db: Session, *, consultant: ConsultantProfile, task: ClientServiceTaskInstance, action: str, note: str, user_id: int) -> tuple[bool, str | None]:
    if not consultant_can_execute_task(consultant=consultant, task=task):
        return False, "This task is not assigned to your consultant profile."
    action = (action or "").strip().lower()
    note = (note or "").strip()
    current = (task.consultant_assignment_status or "not_applicable").lower()
    now = datetime.now(timezone.utc)
    message = None
    if action == "accept" and current == "offered":
        task.consultant_assignment_status = "accepted"
        task.consultant_accepted_at_utc = now
        task.consultant_declined_at_utc = None
        task.consultant_decline_reason = None
        task.status = "pending"
        message = "Consultant accepted the assignment."
    elif action == "decline" and current == "offered":
        if not note:
            return False, "Decline reason is required."
        task.consultant_assignment_status = "declined"
        task.consultant_declined_at_utc = now
        task.consultant_decline_reason = note
        task.status = "pending"
        message = f"Consultant declined the assignment: {note}"
    elif action == "start" and current in {"accepted", "rework_required"}:
        task.consultant_assignment_status = "in_progress"
        task.consultant_started_at_utc = task.consultant_started_at_utc or now
        task.status = "in_progress"
        if current == "rework_required":
            task.rework_status = "in_progress"
        message = "Consultant started work on the assignment."
    elif action == "submit" and current in {"accepted", "in_progress", "rework_required"}:
        if not note:
            return False, "Submission note is required."
        task.consultant_assignment_status = "submitted"
        task.consultant_submitted_at_utc = now
        task.consultant_submission_note = note
        task.status = "pending_review"
        if task.rework_status in {"requested", "in_progress"}:
            task.rework_status = "resolved"
            task.rework_resolved_at_utc = now
        message = f"Consultant submitted the assignment for firm review: {note}"
    else:
        return False, "This action is not allowed for the current assignment status."
    task.updated_by_user_id = user_id
    db.add(ServiceTaskComment(tenant_id=task.tenant_id, branch_id=task.branch_id, subscription_id=task.subscription_id, task_instance_id=task.id, comment_type="consultant_execution", visibility="consultant", message=message, created_by_user_id=user_id))
    return True, None
