from __future__ import annotations

import logging
import os
import threading
import time
from dataclasses import dataclass
from datetime import date, datetime, time as dt_time, timedelta, timezone

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

from app.core.db.common import CommonEngine, CommonSessionLocal
from app.modules.alerts.models import UserAlert
from app.modules.alerts.service import create_alert
from app.modules.clients.models import Client
from app.modules.employees.models import Employee, EmployeeAttendance
from app.modules.services.models import ClientServiceSubscription, ClientServiceTaskInstance

logger = logging.getLogger("audit_firm.notifications")

_COMPLETED_STATUSES = {
    "completed",
    "complete",
    "done",
    "approved",
    "closed",
    "filed",
    "cancelled",
    "inactive",
}

_STARTED = False
_START_LOCK = threading.Lock()


@dataclass(slots=True)
class AutomationResult:
    task_due_today: int = 0
    task_overdue_staff: int = 0
    task_overdue_manager: int = 0
    task_overdue_partner: int = 0
    engagement_due: int = 0
    engagement_overdue: int = 0
    attendance_missing_punchout: int = 0

    @property
    def total_alerts(self) -> int:
        return (
            self.task_due_today
            + self.task_overdue_staff
            + self.task_overdue_manager
            + self.task_overdue_partner
            + self.engagement_due
            + self.engagement_overdue
            + self.attendance_missing_punchout
        )


def _table_exists(table_name: str) -> bool:
    try:
        return CommonEngine.dialect.has_table(CommonEngine.connect(), table_name)
    except Exception:
        return False


def _today_bounds_utc(now_utc: datetime) -> tuple[datetime, datetime]:
    start = datetime.combine(now_utc.date(), dt_time.min, tzinfo=timezone.utc)
    end = start + timedelta(days=1)
    return start, end


def _normalize_status(value: str | None) -> str:
    return (value or "").strip().lower().replace(" ", "_")


def _is_open_status(value: str | None) -> bool:
    return _normalize_status(value) not in _COMPLETED_STATUSES


def _alert_exists_today(
    db: Session,
    *,
    user_id: int,
    alert_type: str,
    title: str,
    target_url: str | None,
    today_start_utc: datetime,
) -> bool:
    q = select(UserAlert.id).where(
        UserAlert.user_id == user_id,
        UserAlert.alert_type == alert_type,
        UserAlert.title == title[:255],
        UserAlert.created_at_utc >= today_start_utc,
    )
    if target_url:
        q = q.where(UserAlert.target_url == target_url)
    else:
        q = q.where(UserAlert.target_url.is_(None))
    return db.execute(q.limit(1)).scalar_one_or_none() is not None


def _create_alert_once(
    db: Session,
    *,
    user_id: int | None,
    title: str,
    message: str | None,
    tenant_id: int | None,
    branch_id: int | None,
    role_context: str | None,
    alert_type: str,
    priority: str,
    target_url: str | None,
    today_start_utc: datetime,
) -> bool:
    if not user_id:
        return False
    if _alert_exists_today(
        db,
        user_id=int(user_id),
        alert_type=alert_type,
        title=title,
        target_url=target_url,
        today_start_utc=today_start_utc,
    ):
        return False
    create_alert(
        db,
        user_id=int(user_id),
        title=title,
        message=message,
        tenant_id=tenant_id,
        branch_id=branch_id,
        role_context=role_context,
        alert_type=alert_type,
        priority=priority,
        target_url=target_url,
        commit=False,
    )
    return True


def _service_label(subscription: ClientServiceSubscription | None, client: Client | None = None) -> str:
    if not subscription:
        return "work item"
    service_name = getattr(getattr(subscription, "catalogue", None), "service_name", None)
    client_name = getattr(client or getattr(subscription, "client", None), "client_name", None)
    period = getattr(subscription, "financial_year", None) or getattr(subscription, "assessment_year", None)
    parts = [p for p in [service_name, client_name, period] if p]
    return " - ".join(parts) if parts else f"Engagement #{subscription.id}"


def run_notification_escalation_once(*, db: Session | None = None, now: datetime | None = None) -> AutomationResult:
    """Create daily alerts for due work, overdue work and basic escalations.

    This function is intentionally idempotent for the current UTC day. It checks
    the existing user_alerts table before inserting, so repeated scheduler runs
    do not flood users with duplicate alerts.
    """

    owns_session = db is None
    session = db or CommonSessionLocal()
    now_utc = now.astimezone(timezone.utc) if now else datetime.now(timezone.utc)
    today = now_utc.date()
    today_start_utc, _ = _today_bounds_utc(now_utc)
    result = AutomationResult()

    try:
        # Phase 4: materialize registration renewal/update work before alert processing.
        try:
            if _table_exists("registration_renewal_tasks"):
                from app.modules.registrations.service import run_registration_lifecycle_once
                run_registration_lifecycle_once(session, today=now_utc.date())
        except Exception:
            logger.exception("Registration lifecycle automation failed")

        if not _table_exists("user_alerts"):
            logger.warning("Phase 7O skipped: user_alerts table is not available. Apply Phase 7H first.")
            return result

        # 1) Staff task due today and overdue alerts.
        task_rows = session.execute(
            select(ClientServiceTaskInstance, ClientServiceSubscription, Client)
            .join(ClientServiceSubscription, ClientServiceTaskInstance.subscription_id == ClientServiceSubscription.id)
            .join(Client, ClientServiceTaskInstance.client_id == Client.id)
            .where(
                ClientServiceTaskInstance.is_active.is_(True),
                ClientServiceTaskInstance.assigned_to_user_id.is_not(None),
                ClientServiceTaskInstance.internal_target_date.is_not(None),
                ClientServiceTaskInstance.internal_target_date <= today,
            )
            .order_by(ClientServiceTaskInstance.internal_target_date.asc(), ClientServiceTaskInstance.id.asc())
        ).all()

        for task, subscription, client in task_rows:
            if not _is_open_status(task.status):
                continue
            target_url = f"/work/engagements/{subscription.id}"
            label = _service_label(subscription, client)
            task_due = task.internal_target_date
            if task_due == today:
                title = f"Task due today: {task.task_name}"
                if _create_alert_once(
                    session,
                    user_id=task.assigned_to_user_id,
                    title=title,
                    message=f"{label} has a task due today. Please open the work details and update the task status.",
                    tenant_id=task.tenant_id,
                    branch_id=task.branch_id,
                    role_context="staff",
                    alert_type="task_due",
                    priority="high" if task.priority in {"high", "critical"} else "normal",
                    target_url=target_url,
                    today_start_utc=today_start_utc,
                ):
                    result.task_due_today += 1
                continue

            overdue_days = max(1, (today - task_due).days)
            title = f"Task overdue: {task.task_name}"
            if _create_alert_once(
                session,
                user_id=task.assigned_to_user_id,
                title=title,
                message=f"{label} is overdue by {overdue_days} day(s). Please complete or update the task status.",
                tenant_id=task.tenant_id,
                branch_id=task.branch_id,
                role_context="staff",
                alert_type="task_overdue",
                priority="critical" if overdue_days >= 3 else "high",
                target_url=target_url,
                today_start_utc=today_start_utc,
            ):
                result.task_overdue_staff += 1

            if subscription.assigned_manager_user_id and overdue_days >= 1:
                if _create_alert_once(
                    session,
                    user_id=subscription.assigned_manager_user_id,
                    title=f"Team task overdue: {task.task_name}",
                    message=f"{label} has an overdue task assigned to staff. Overdue by {overdue_days} day(s).",
                    tenant_id=task.tenant_id,
                    branch_id=task.branch_id,
                    role_context="manager",
                    alert_type="task_overdue",
                    priority="high",
                    target_url=target_url,
                    today_start_utc=today_start_utc,
                ):
                    result.task_overdue_manager += 1

            partner_user_id = subscription.assigned_partner_user_id or subscription.review_partner_user_id
            if partner_user_id and overdue_days >= 3:
                if _create_alert_once(
                    session,
                    user_id=partner_user_id,
                    title=f"Escalation: task overdue for {overdue_days} days",
                    message=f"{label} has a task pending beyond escalation threshold: {task.task_name}.",
                    tenant_id=task.tenant_id,
                    branch_id=task.branch_id,
                    role_context="partner",
                    alert_type="task_overdue",
                    priority="critical",
                    target_url=target_url,
                    today_start_utc=today_start_utc,
                ):
                    result.task_overdue_partner += 1

        # 2) Engagement/service due and overdue alerts for responsible users and client portal user.
        sub_rows = session.execute(
            select(ClientServiceSubscription, Client)
            .join(Client, ClientServiceSubscription.client_id == Client.id)
            .where(
                ClientServiceSubscription.is_active.is_(True),
                ClientServiceSubscription.current_due_date.is_not(None),
                ClientServiceSubscription.current_due_date <= today + timedelta(days=2),
            )
            .order_by(ClientServiceSubscription.current_due_date.asc(), ClientServiceSubscription.id.asc())
        ).all()

        for subscription, client in sub_rows:
            if not _is_open_status(subscription.status):
                continue
            due_date = subscription.current_due_date
            target_url = f"/work/engagements/{subscription.id}"
            label = _service_label(subscription, client)
            recipient_map = [
                (subscription.assigned_staff_user_id, "staff"),
                (subscription.assigned_manager_user_id, "manager"),
                (subscription.assigned_partner_user_id or subscription.review_partner_user_id, "partner"),
                (getattr(client, "portal_user_id", None), "client"),
            ]
            seen: set[int] = set()
            if due_date < today:
                overdue_days = (today - due_date).days
                title = f"Engagement overdue: {label}"
                message = f"Due date was {due_date.isoformat()} and is overdue by {overdue_days} day(s)."
                alert_type = "task_overdue"
                priority = "critical" if overdue_days >= 3 else "high"
            else:
                days_left = (due_date - today).days
                title = f"Due date approaching: {label}"
                message = "Due today." if days_left == 0 else f"Due in {days_left} day(s), on {due_date.isoformat()}."
                alert_type = "task_due"
                priority = "high" if days_left == 0 else "normal"
            for user_id, role_context in recipient_map:
                if not user_id or int(user_id) in seen:
                    continue
                seen.add(int(user_id))
                if _create_alert_once(
                    session,
                    user_id=user_id,
                    title=title,
                    message=message,
                    tenant_id=subscription.tenant_id,
                    branch_id=subscription.branch_id,
                    role_context=role_context,
                    alert_type=alert_type,
                    priority=priority,
                    target_url=target_url,
                    today_start_utc=today_start_utc,
                ):
                    if due_date < today:
                        result.engagement_overdue += 1
                    else:
                        result.engagement_due += 1

        # 3) Missing punch-out reminders from yesterday.
        yesterday = today - timedelta(days=1)
        attendance_rows = session.execute(
            select(EmployeeAttendance, Employee)
            .join(Employee, EmployeeAttendance.employee_id == Employee.id)
            .where(
                EmployeeAttendance.attendance_date == yesterday,
                EmployeeAttendance.punch_in_utc.is_not(None),
                EmployeeAttendance.punch_out_utc.is_(None),
                Employee.user_id.is_not(None),
            )
        ).all()
        for attendance, employee in attendance_rows:
            title = "Attendance punch-out missing"
            target_url = "/employee/attendance"
            if _create_alert_once(
                session,
                user_id=employee.user_id,
                title=title,
                message=f"Punch-out is missing for {yesterday.isoformat()}. Please regularise or contact your manager.",
                tenant_id=attendance.tenant_id,
                branch_id=attendance.branch_id,
                role_context="staff",
                alert_type="attendance",
                priority="normal",
                target_url=target_url,
                today_start_utc=today_start_utc,
            ):
                result.attendance_missing_punchout += 1
            if employee.reporting_manager_user_id:
                if _create_alert_once(
                    session,
                    user_id=employee.reporting_manager_user_id,
                    title=f"Team attendance punch-out missing: {employee.full_name}",
                    message=f"{employee.full_name} has no punch-out for {yesterday.isoformat()}.",
                    tenant_id=attendance.tenant_id,
                    branch_id=attendance.branch_id,
                    role_context="manager",
                    alert_type="attendance",
                    priority="normal",
                    target_url="/employees/attendance",
                    today_start_utc=today_start_utc,
                ):
                    result.attendance_missing_punchout += 1

        session.commit()
        logger.info("Phase 7O notification automation completed: %s alert(s) created", result.total_alerts)
        return result
    except Exception:
        session.rollback()
        logger.exception("Phase 7O notification automation failed")
        return result
    finally:
        if owns_session:
            session.close()


def _scheduler_loop(interval_seconds: int, initial_delay_seconds: int) -> None:
    if initial_delay_seconds > 0:
        time.sleep(initial_delay_seconds)
    while True:
        run_notification_escalation_once()
        time.sleep(interval_seconds)


def start_notification_scheduler() -> None:
    """Start the lightweight Phase 7O background scheduler once per process.

    Environment variables:
      AF_ALERT_AUTOMATION_ENABLED=false   disables the scheduler
      AF_ALERT_AUTOMATION_INTERVAL_SECONDS=3600 controls repeat interval
      AF_ALERT_AUTOMATION_INITIAL_DELAY_SECONDS=20 controls first run delay
    """

    global _STARTED
    enabled = (os.getenv("AF_ALERT_AUTOMATION_ENABLED", "true") or "true").strip().lower()
    if enabled in {"0", "false", "no", "off"}:
        logger.info("Phase 7O notification automation disabled by environment setting")
        return

    with _START_LOCK:
        if _STARTED:
            return
        interval = int(os.getenv("AF_ALERT_AUTOMATION_INTERVAL_SECONDS", "3600") or "3600")
        initial_delay = int(os.getenv("AF_ALERT_AUTOMATION_INITIAL_DELAY_SECONDS", "20") or "20")
        interval = max(300, interval)
        initial_delay = max(0, initial_delay)
        thread = threading.Thread(
            target=_scheduler_loop,
            args=(interval, initial_delay),
            name="phase7o-notification-escalation",
            daemon=True,
        )
        thread.start()
        _STARTED = True
        logger.info("Phase 7O notification automation started with interval=%s seconds", interval)
