from __future__ import annotations

from datetime import date, datetime, timedelta
from calendar import monthrange
from typing import Dict, List, Tuple, Optional

from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for
from flask_login import login_required, current_user
from sqlalchemy import and_, or_, func

from extensions import db
from models import (
    Employee,
    Attendance,
    LeaveRequest,
    LeaveType,
    Holiday,          # your model exists
    PublicHoliday,    # your model exists (optional)
    WeekendSettings,
    Payroll,
    MonthlyPayroll,
)

team_attendance_bp = Blueprint("team_attendance", __name__, url_prefix="/attendance")


# -------------------------------
# Helpers
# -------------------------------

ALLOWED_EMPLOYEE_STATUSES = ("active", "notice period", "notice_period")


def _is_hr_or_admin() -> bool:
    role = (getattr(getattr(current_user, "role", None), "name", "") or "").lower()
    return role in ("hr", "admin", "superadmin")


def _is_manager() -> bool:
    # if someone has reportees, treat them as manager
    # (works even if role name isn't "manager")
    try:
        return current_user.reportees.count() > 0
    except Exception:
        return False


def _month_str_to_year_month(month_str: str) -> Tuple[int, int]:
    # month_str: "YYYY-MM"
    y, m = month_str.split("-")
    return int(y), int(m)


def _month_range(month_str: str) -> Tuple[date, date]:
    y, m = _month_str_to_year_month(month_str)
    last = monthrange(y, m)[1]
    return date(y, m, 1), date(y, m, last)


def _iter_dates(d1: date, d2: date) -> List[date]:
    out = []
    cur = d1
    while cur <= d2:
        out.append(cur)
        cur += timedelta(days=1)
    return out


def _month_is_frozen(month_str: str) -> bool:
    """
    Consider month frozen if either Payroll or MonthlyPayroll exists for that month.
    - Payroll.month is 'YYYY-MM'
    - MonthlyPayroll has year/month ints
    """
    y, m = _month_str_to_year_month(month_str)

    exists_payroll = db.session.query(Payroll.id).filter(Payroll.month == month_str).first() is not None
    exists_monthly = (
        db.session.query(MonthlyPayroll.id)
        .filter(MonthlyPayroll.year == y, MonthlyPayroll.month == m)
        .first()
        is not None
    )
    return bool(exists_payroll or exists_monthly)


def _get_visible_employees(filters: dict) -> List[Employee]:
    """
    Manager: only reportees (plus optionally themselves if you want).
    HR/Admin: all active employees (with filters).
    """
    q = Employee.query.filter(Employee.status.in_(ALLOWED_EMPLOYEE_STATUSES))

    if not _is_hr_or_admin():
        # Manager view: only direct reportees
        q = q.filter(Employee.reporting_manager_id == current_user.id)

    # Filters
    search = (filters.get("search") or "").strip()
    dept_id = filters.get("department_id")
    manager_id = filters.get("manager_id")

    if search:
        like = f"%{search}%"
        q = q.filter(or_(
            Employee.full_name.ilike(like),
            Employee.employee_code.ilike(like),
            Employee.email.ilike(like),
        ))

    if dept_id and dept_id.isdigit():
        q = q.filter(Employee.department_id == int(dept_id))

    # HR/Admin can filter by manager
    if _is_hr_or_admin() and manager_id and manager_id.isdigit():
        q = q.filter(Employee.reporting_manager_id == int(manager_id))

    return q.order_by(Employee.full_name.asc()).all()


def _get_holiday_map(start: date, end: date) -> Dict[date, str]:
    """
    Supports both Holiday and PublicHoliday if present in DB.
    If both exist for same date, PublicHoliday name wins.
    """
    holiday_map: Dict[date, str] = {}

    # Holiday table
    rows = Holiday.query.filter(Holiday.date.between(start, end)).all()
    for h in rows:
        holiday_map[h.date] = h.name or "Holiday"

    # PublicHoliday table (optional)
    try:
        ph_rows = PublicHoliday.query.filter(PublicHoliday.date.between(start, end)).all()
        for h in ph_rows:
            holiday_map[h.date] = h.name or "Holiday"
    except Exception:
        pass

    return holiday_map


def _is_weekend(d: date, ws: Optional[WeekendSettings]) -> Tuple[bool, str]:
    """
    Returns (is_weekend_off, label)
    - Sunday uses ws.sunday ('off'/'working'/'half-day')
    - Saturday uses first_saturday...fifth_saturday
    """
    if not ws:
        # default: Sunday off, Saturday off
        if d.weekday() == 6:
            return True, "Weekend"
        if d.weekday() == 5:
            return True, "Weekend"
        return False, ""

    if d.weekday() == 6:  # Sunday
        return (ws.sunday or "off") != "working", "Weekend"

    if d.weekday() == 5:  # Saturday
        # which saturday of month?
        week_of_month = ((d.day - 1) // 7) + 1  # 1..5
        key = f"{['first','second','third','fourth','fifth'][week_of_month-1]}_saturday"
        val = getattr(ws, key, "off") or "off"
        return val != "working", "Weekend"

    return False, ""


def _get_leave_days_map(employee_ids: List[int], start: date, end: date) -> Dict[Tuple[int, date], dict]:
    """
    Build map: (employee_id, day) -> {type_name, half_day(bool)}
    Only for approved leaves.
    """
    if not employee_ids:
        return {}

    leaves = (
        LeaveRequest.query
        .join(LeaveType, LeaveType.id == LeaveRequest.leave_type_id)
        .filter(
            LeaveRequest.employee_id.in_(employee_ids),
            LeaveRequest.status.in_(["final_approved", "manager_approved"]),
            LeaveRequest.start_date <= end,
            LeaveRequest.end_date >= start,
        )
        .all()
    )

    out: Dict[Tuple[int, date], dict] = {}
    for lr in leaves:
        cur = lr.start_date
        while cur <= lr.end_date:
            # NOTE: if your leave module has RH rules etc, you can extend here.
            out[(lr.employee_id, cur)] = {
                "leave_type": (lr.leave_type.name if lr.leave_type else "Leave"),
                "half_day": bool(lr.half_day) and (lr.start_date == lr.end_date == cur),
                "session": lr.half_day_session,
            }
            cur += timedelta(days=1)

    return out


# -------------------------------
# Routes
# -------------------------------

@team_attendance_bp.route("/team", methods=["GET"])
@login_required
def team_attendance():
    # month default = current month
    month_str = (request.args.get("month") or date.today().strftime("%Y-%m")).strip()
    try:
        start, end = _month_range(month_str)
    except Exception:
        month_str = date.today().strftime("%Y-%m")
        start, end = _month_range(month_str)

    filters = {
        "search": request.args.get("search", ""),
        "department_id": request.args.get("department_id", ""),
        "manager_id": request.args.get("manager_id", ""),
    }

    employees = _get_visible_employees(filters)
    employee_ids = [e.id for e in employees]

    days = _iter_dates(start, end)
    month_frozen = _month_is_frozen(month_str)

    # Attendance rows
    att_rows = []
    if employee_ids:
        att_rows = (
            Attendance.query
            .filter(
                Attendance.employee_id.in_(employee_ids),
                Attendance.date.between(start, end),
            )
            .all()
        )
    att_map: Dict[Tuple[int, date], Attendance] = {(a.employee_id, a.date): a for a in att_rows}

    # Holidays
    holiday_map = _get_holiday_map(start, end)

    # Weekend Settings (single row)
    ws = WeekendSettings.query.order_by(WeekendSettings.updated_on.desc()).first()

    # Leaves
    leave_map = _get_leave_days_map(employee_ids, start, end)

    # Build view model per employee
    team_cards = []
    for emp in employees:
        per_day = []
        for d in days:
            key = (emp.id, d)
            att = att_map.get(key)
            leave = leave_map.get(key)
            holiday_name = holiday_map.get(d)
            is_weekend, weekend_label = _is_weekend(d, ws)

            cell = {
                "date": d,
                "att": att,
                "leave": leave,
                "holiday": holiday_name,
                "is_weekend": is_weekend,
                "weekend_label": weekend_label,
            }
            per_day.append(cell)

        team_cards.append({
            "employee": emp,
            "days": per_day
        })

    # Filter dropdown datasets
    departments = db.session.query(Employee.department_id, func.count(Employee.id)).group_by(Employee.department_id).all()
    dept_ids = [d[0] for d in departments if d[0]]
    dept_list = []
    if dept_ids:
        from models import Department
        dept_list = Department.query.filter(Department.id.in_(dept_ids)).order_by(Department.name.asc()).all()

    from sqlalchemy import distinct

    managers_list = []
    if _is_hr_or_admin():
        managers_list = (
            Employee.query
            .filter(Employee.status.in_(ALLOWED_EMPLOYEE_STATUSES))
            .filter(
                Employee.id.in_(
                    db.session.query(distinct(Employee.reporting_manager_id))
                    .filter(Employee.reporting_manager_id.isnot(None))
                    .subquery()
                )
            )
            .order_by(Employee.full_name.asc())
            .all()
        )

    return render_template(
        "team_attendance.html",
        month_str=month_str,
        start=start,
        end=end,
        days=days,
        team_cards=team_cards,
        month_frozen=month_frozen,
        is_hr_or_admin=_is_hr_or_admin(),
        is_manager=_is_manager(),
        dept_list=dept_list,
        managers_list=managers_list,
        filters=filters,
    )


@team_attendance_bp.route("/team/update", methods=["POST"])
@login_required
def team_attendance_update():
    """
    Manual update for a single employee/day.
    Block if payroll is frozen for that month.
    Access:
      - HR/Admin: can update anyone
      - Manager: can update only their reportees
    """
    data = request.get_json(force=True) or {}
    employee_id = int(data.get("employee_id") or 0)
    date_str = (data.get("date") or "").strip()

    status = (data.get("status") or "").strip()  # present/absent/paid_leave/lop/half_day etc (your system)
    source = (data.get("source") or "Manual").strip()
    remarks = (data.get("remarks") or "").strip()
    note = (data.get("note") or "").strip()

    check_in = (data.get("check_in_time") or "").strip()   # "HH:MM"
    check_out = (data.get("check_out_time") or "").strip() # "HH:MM"
    working_hours = data.get("working_hours")

    if not employee_id or not date_str:
        return jsonify({"ok": False, "message": "Invalid input"}), 400

    try:
        d = datetime.strptime(date_str, "%Y-%m-%d").date()
    except Exception:
        return jsonify({"ok": False, "message": "Invalid date"}), 400

    month_str = d.strftime("%Y-%m")
    if _month_is_frozen(month_str):
        return jsonify({"ok": False, "message": f"Month {month_str} is frozen. Updates are not allowed."}), 403

    emp = Employee.query.get(employee_id)
    if not emp:
        return jsonify({"ok": False, "message": "Employee not found"}), 404

    # Authorization
    if not _is_hr_or_admin():
        # only allow manager to edit reportees
        if emp.reporting_manager_id != current_user.id:
            return jsonify({"ok": False, "message": "Not allowed"}), 403

    # Parse times (optional)
    def _parse_time(t: str):
        if not t:
            return None
        return datetime.strptime(t, "%H:%M").time()

    ci = None
    co = None
    try:
        ci = _parse_time(check_in)
        co = _parse_time(check_out)
    except Exception:
        return jsonify({"ok": False, "message": "Invalid time format. Use HH:MM."}), 400

    # Working hours
    wh = None
    if working_hours not in (None, "", "null"):
        try:
            wh = float(working_hours)
        except Exception:
            return jsonify({"ok": False, "message": "Invalid working hours"}), 400

    # Upsert attendance
    att = Attendance.query.filter_by(employee_id=employee_id, date=d).first()
    if not att:
        att = Attendance(employee_id=employee_id, date=d)
        db.session.add(att)

    # Apply updates
    if status:
        att.status = status
    att.source = source or att.source
    att.remarks = remarks
    att.note = note
    att.check_in_time = ci
    att.check_out_time = co
    if wh is not None:
        att.working_hours = wh

    # Optional auto-calc hours if times provided and hours not given
    if wh is None and ci and co:
        dt_ci = datetime.combine(d, ci)
        dt_co = datetime.combine(d, co)
        if dt_co >= dt_ci:
            att.working_hours = round((dt_co - dt_ci).total_seconds() / 3600.0, 2)

    db.session.commit()
    return jsonify({"ok": True, "message": "Attendance updated"})


# For menu mapping convenience
# endpoint: team_attendance.team_attendance
# url: /attendance/team