import os
from typing import Optional
from functools import wraps
from utils.push import send_push
from flask import Blueprint, request, jsonify, current_app, url_for
from werkzeug.utils import secure_filename
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
from routes.payroll import _salary_slip_pdf_response
from datetime import datetime, date, timedelta
from sqlalchemy import and_, or_, extract, func, desc
from utils.attendance_utils import sync_leave_to_attendance
from urllib.parse import urlparse, urlunparse

from extensions import db
from models import (
    Employee,
    BiometricPunch,
    BrandingSetting,
    BroadcastMessage,
    Holiday,
    LeaveRequest,
    LeaveType,
    LeaveBalance,
    LeaveApprovalLog,
    LeaveSettings,
    Payroll,
    Notification,
    DeviceToken,
    Department,
    Role,
)
from sqlalchemy.orm import joinedload
from routes.payroll import (
    _normalize_month,
    _has_filled_feedback,
    download_salary_slip_pdf,
)

api_bp = Blueprint("api", __name__, url_prefix="/api")


# ---------------------------------------------------------------------
# AUTH HELPERS
# ---------------------------------------------------------------------
def _serializer(secret_key: str):
    return URLSafeTimedSerializer(secret_key, salt="hrsoft-mobile")


def require_api_auth(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        auth = request.headers.get("Authorization", "")
        if not auth.startswith("Bearer "):
            return jsonify({"error": "Missing token"}), 401

        token = auth.replace("Bearer ", "").strip()

        secret = current_app.config.get("SECRET_KEY")
        if not secret:
            return jsonify({"error": "Server misconfigured: SECRET_KEY missing"}), 500

        s = _serializer(secret)

        try:
            payload = s.loads(token, max_age=60 * 60 * 24 * 30)  # 30 days
        except SignatureExpired:
            return jsonify({"error": "Token expired"}), 401
        except BadSignature:
            return jsonify({"error": "Invalid token"}), 401

        emp_id = payload.get("employee_id")
        emp = Employee.query.get(emp_id)
        if not emp:
            return jsonify({"error": "Invalid user"}), 401

        request.api_employee = emp
        return fn(*args, **kwargs)

    return wrapper


def _role_name(emp: Employee) -> str:
    return (emp.role.name if emp.role else "").strip().lower()


def _status_allowed_expr():
    return or_(
        Employee.status == None,  # noqa
        Employee.status.in_(["Active", "Notice", "active", "notice"]),
    )


# ---------------------------------------------------------------------
# Notification/URL/CANONICAL HELPERS
# ---------------------------------------------------------------------
def _preferred_scheme() -> str:
    return (
        current_app.config.get("PREFERRED_URL_SCHEME")
        or (
            "https"
            if request.headers.get("X-Forwarded-Proto", "").lower() == "https"
            else request.scheme
            or "https"
        )
    ).lower()


def _canonical_host_and_scheme():
    external = (current_app.config.get("EXTERNAL_BASE_URL") or "").strip()
    if external:
        p = urlparse(external if "://" in external else f"https://{external}")
        if p.netloc:
            return (p.scheme or _preferred_scheme(), p.netloc)

    server_name = (current_app.config.get("SERVER_NAME") or "").strip()
    if server_name:
        return (_preferred_scheme(), server_name)

    return (_preferred_scheme(), request.host)


def _canonicalize_link(raw: Optional[str]) -> Optional[str]:
    if not raw:
        return None
    raw = raw.strip()
    scheme, host = _canonical_host_and_scheme()

    if raw.lower().startswith(("http://", "https://")):
        p = urlparse(raw)
        p = p._replace(scheme=scheme, netloc=host)
        return urlunparse(p)

    if raw.startswith("/"):
        return f"{scheme}://{host}{raw}"

    return f"{scheme}://{host}/{raw.lstrip('/')}"


def _notif_base_query(emp: Employee):
    return Notification.query.filter(
        or_(Notification.employee_id == emp.id, Notification.employee_id.is_(None))
    )


# ---------------------------------------------------------------------
# LEAVE HELPERS (API)
# ---------------------------------------------------------------------
def _leave_year_range():
    settings = LeaveSettings.query.first()
    if settings and getattr(settings, "leave_year_start", None) and getattr(settings, "leave_year_end", None):
        return settings.leave_year_start, settings.leave_year_end

    today = datetime.today().date()
    fy_start = date(today.year if today.month >= 4 else today.year - 1, 4, 1)
    fy_end = date(fy_start.year + 1, 3, 31)
    return fy_start, fy_end


def _fy_label():
    fy_start, _ = _leave_year_range()
    return int(fy_start.year)


def _lb_year_field():
    if hasattr(LeaveBalance, "year"):
        return "year"
    if hasattr(LeaveBalance, "credit_year"):
        return "credit_year"
    return "year"


def _get_balance_row(employee_id: int, leave_type_id: int, fy_label: int):
    fld = _lb_year_field()
    q = LeaveBalance.query.filter_by(employee_id=employee_id, leave_type_id=leave_type_id)
    q = q.filter(getattr(LeaveBalance, fld) == fy_label)
    return q.first()


def _sum_rh_in_flight_days(employee_id: int, ly_start: date, ly_end: date) -> float:
    in_flight_statuses = ["pending", "manager_approved", "final_approved"]
    rh_sum = (
        db.session.query(func.coalesce(func.sum(func.coalesce(LeaveRequest.total_days, 1.0)), 0.0))
        .filter(
            LeaveRequest.employee_id == employee_id,
            LeaveRequest.leave_type.has(code="RH"),
            LeaveRequest.start_date >= ly_start,
            LeaveRequest.end_date <= ly_end,
            LeaveRequest.status.in_(in_flight_statuses),
        )
        .scalar()
    )
    return float(rh_sum or 0.0)


def _get_optional_holiday_dates(start_dt: date, end_dt: date, employee: Optional[Employee] = None) -> set[date]:
    hols = Holiday.query.filter(Holiday.date >= start_dt, Holiday.date <= end_dt).all()
    by_date = {}
    for h in hols:
        by_date.setdefault(h.date, []).append(h)

    whitelist: set[date] = set()
    emp_loc = getattr(employee, "location", None)

    for dt, items in by_date.items():
        specific = next((x for x in items if emp_loc and x.location == emp_loc), None)
        if specific:
            if (specific.type or "").lower() == "optional":
                whitelist.add(dt)
            continue

        all_row = next((x for x in items if x.location == "ALL"), None)
        if all_row and (all_row.type or "").lower() == "optional":
            whitelist.add(dt)

    return whitelist


def _get_applicable_holiday_for(employee: Employee, dt: date):
    q = Holiday.query.filter_by(date=dt)
    emp_loc = getattr(employee, "location", None)
    if emp_loc:
        h = q.filter_by(location=emp_loc).first()
        if h:
            return h
    return q.filter_by(location="ALL").first()


# ---------------------------------------------------------------------
# PUSH NOTIFICATION HELPERS
# ---------------------------------------------------------------------
def _deactivate_tokens(bad_tokens: list[str]):
    bad_tokens = [t for t in (bad_tokens or []) if t]
    if not bad_tokens:
        return
    DeviceToken.query.filter(DeviceToken.token.in_(bad_tokens)).update(
        {DeviceToken.is_active: False, DeviceToken.last_seen_at: datetime.utcnow()},
        synchronize_session=False
    )
    db.session.commit()


def _safe_push_send(token_list, title: str, message: str, link: Optional[str], notif_type: str):
    try:
        if token_list:
            return send_push(
                token_list,
                title=title,
                body=message,
                data={"link": link or "", "type": notif_type},
                on_invalid_tokens=_deactivate_tokens,
            )
        return {"sent": 0, "failed": 0, "invalid": 0}
    except Exception:
        current_app.logger.exception("Push send failed")
        return {"sent": 0, "failed": len(token_list or []), "invalid": 0}


def _push_to_employee(
    employee_id: int,
    title: str,
    message: str,
    link: Optional[str] = None,
    notif_type: str = "info",
):
    tokens = (
        DeviceToken.query
        .filter(DeviceToken.employee_id == employee_id, DeviceToken.is_active.is_(True))
        .with_entities(DeviceToken.token)
        .all()
    )
    token_list = [t[0] for t in tokens if t and t[0]]

    db.session.add(Notification(
        employee_id=employee_id,
        title=title[:255],
        message=message,
        type=notif_type,
        link=link,
        is_read=False,
    ))
    db.session.commit()

    return _safe_push_send(token_list, title, message, link, notif_type)


def _push_to_many(
    employee_ids: list[int],
    title: str,
    message: str,
    link: Optional[str] = None,
    notif_type: str = "info",
):
    if not employee_ids:
        return {"sent": 0, "failed": 0, "invalid": 0}

    for eid in employee_ids:
        db.session.add(Notification(
            employee_id=eid,
            title=title[:255],
            message=message,
            type=notif_type,
            link=link,
            is_read=False,
        ))
    db.session.commit()

    tokens = (
        DeviceToken.query
        .filter(DeviceToken.employee_id.in_(employee_ids), DeviceToken.is_active.is_(True))
        .with_entities(DeviceToken.token)
        .all()
    )
    token_list = [t[0] for t in tokens if t and t[0]]

    return _safe_push_send(token_list, title, message, link, notif_type)


# ---------------------------------------------------------------------
# BASIC ENDPOINTS
# ---------------------------------------------------------------------
@api_bp.get("/health")
def health():
    return jsonify({"ok": True}), 200


@api_bp.get("/me")
@require_api_auth
def api_me():
    emp = request.api_employee
    return jsonify(
        {
            "id": emp.id,
            "employee_code": emp.employee_code,
            "full_name": emp.full_name,
            "role": emp.role.name if emp.role else None,
        }
    ), 200


@api_bp.post("/auth/login")
def api_login():
    data = request.get_json(silent=True) or {}

    employee_code = (data.get("employee_code") or "").strip()
    password = data.get("password") or ""

    if not employee_code or not password:
        return jsonify({"error": "employee_code and password are required"}), 400

    employee = Employee.query.filter_by(employee_code=employee_code).first()
    if not employee or not employee.check_password(password):
        return jsonify({"error": "Invalid credentials"}), 401

    emp_status = (employee.status or "").strip().lower()
    lwd = getattr(employee, "exit_date", None)

    if emp_status in ("inactive", "terminated", "exited"):
        return jsonify({"error": "Your account is inactive. Please contact HR."}), 403

    if lwd and isinstance(lwd, datetime):
        lwd = lwd.date()

    if lwd and lwd <= datetime.utcnow().date():
        return jsonify({"error": "Your employment has ended. Please contact HR."}), 403

    if not employee.role or not getattr(employee.role, "name", None):
        return jsonify({"error": "Role not assigned. Please contact HR."}), 403

    secret = current_app.config.get("SECRET_KEY")
    if not secret:
        return jsonify({"error": "Server misconfigured: SECRET_KEY missing"}), 500

    s = _serializer(secret)
    token = s.dumps({"employee_id": employee.id})

    return jsonify(
        {
            "token": token,
            "employee": {
                "id": employee.id,
                "employee_code": employee.employee_code,
                "full_name": employee.full_name,
                "role": employee.role.name,
            },
        }
    ), 200


# ---------------------------------------------------------------------
# BRANDING
# ---------------------------------------------------------------------
@api_bp.get("/branding")
def api_branding():
    b = BrandingSetting.query.get(1)
    data = {
        "sidebar_bg": "#0B1220",
        "topbar_bg": "#FFFFFF",
        "accent": "#F97316",
        "logo_url": None,
    }
    if b:
        data.update(
            {
                "sidebar_bg": b.sidebar_bg or data["sidebar_bg"],
                "topbar_bg": b.topbar_bg or data["topbar_bg"],
                "accent": b.accent or data["accent"],
                "logo_url": b.logo_url,
            }
        )
    return jsonify(data), 200


# ---------------------------------------------------------------------
# SELF PUNCH (MOBILE)
# ---------------------------------------------------------------------
def _ensure_dir(path: str):
    os.makedirs(path, exist_ok=True)


def _save_punch_photo(file_storage, employee_id: int) -> str:
    base_upload = current_app.config.get("UPLOAD_FOLDER") or "uploads"
    folder = os.path.join(base_upload, "self_punch", str(employee_id))
    _ensure_dir(folder)

    ext = os.path.splitext(file_storage.filename or "")[1].lower()
    if ext not in [".jpg", ".jpeg", ".png", ".webp"]:
        ext = ".jpg"

    ts = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
    filename = secure_filename(f"{ts}_cam{ext}")
    full_path = os.path.join(folder, filename)

    file_storage.save(full_path)

    rel = os.path.relpath(full_path, base_upload)
    return rel.replace("\\", "/")


@api_bp.post("/punch/in")
@require_api_auth
def api_punch_in():
    return _handle_mobile_punch("in")


@api_bp.post("/punch/out")
@require_api_auth
def api_punch_out():
    return _handle_mobile_punch("out")


def _handle_mobile_punch(direction: str):
    emp = request.api_employee

    lat = request.form.get("latitude")
    lon = request.form.get("longitude")
    note = request.form.get("location_note") or ""
    photo = request.files.get("photo")

    if not photo:
        return jsonify({"error": "photo is required"}), 400
    if not lat or not lon:
        return jsonify({"error": "latitude and longitude are required"}), 400

    try:
        lat_f = float(lat)
        lon_f = float(lon)
    except ValueError:
        return jsonify({"error": "Invalid latitude/longitude"}), 400

    rel_photo_path = _save_punch_photo(photo, emp.id)

    p = BiometricPunch(
        employee_id=emp.id,
        external_employee_code=emp.employee_code,
        punch_time=datetime.utcnow(),
        direction=direction,
        source_type="self",
        source_name="self",
        latitude=lat_f,
        longitude=lon_f,
        location_note=note[:255],
        photo_path=rel_photo_path,
        photo_taken_at=datetime.utcnow(),
        photo_is_fresh=True,
        approval_status="pending",
    )
    db.session.add(p)
    db.session.commit()

    return jsonify(
        {
            "ok": True,
            "punch_id": p.id,
            "approval_status": p.approval_status,
            "photo_path": p.photo_path,
            "direction": direction,
            "punch_time": p.punch_time.isoformat(),
        }
    ), 200


# ---------------------------------------------------------------------
# SELF PUNCH APPROVALS (MOBILE)
# ---------------------------------------------------------------------
@api_bp.get("/approvals/self-punch")
@require_api_auth
def api_list_self_punch_approvals():
    emp = request.api_employee
    role = _role_name(emp)

    status = (request.args.get("status") or "pending").lower().strip()
    qdate = (request.args.get("date") or "").strip()

    query = BiometricPunch.query.filter(BiometricPunch.source_name == "self")

    if status != "all":
        query = query.filter(BiometricPunch.approval_status == status)

    if qdate:
        try:
            d = datetime.strptime(qdate, "%Y-%m-%d").date()
            start = datetime(d.year, d.month, d.day, 0, 0, 0)
            end = datetime(d.year, d.month, d.day, 23, 59, 59)
            query = query.filter(BiometricPunch.punch_time.between(start, end))
        except ValueError:
            return jsonify({"error": "Invalid date. Use YYYY-MM-DD"}), 400

    if role not in ("admin", "hr", "hr admin", "demo admin"):
        query = (
            query.join(Employee, Employee.id == BiometricPunch.employee_id)
            .filter(Employee.reporting_manager_id == emp.id)
        )

    punches = query.order_by(BiometricPunch.punch_time.desc()).limit(200).all()

    def _photo_url(photo_path: Optional[str]):
        if not photo_path:
            return None
        return url_for("uploaded_document", filename=photo_path, _external=True)

    return jsonify(
        {
            "items": [
                {
                    "id": p.id,
                    "employee_id": p.employee_id,
                    "employee_code": p.external_employee_code,
                    "employee_name": p.employee.full_name if p.employee else None,
                    "punch_time": p.punch_time.isoformat(),
                    "direction": (p.direction or "").lower(),
                    "status": (p.approval_status or "").lower(),
                    "latitude": p.latitude,
                    "longitude": p.longitude,
                    "location_note": p.location_note,
                    "photo_path": p.photo_path,
                    "photo_url": _photo_url(p.photo_path),
                }
                for p in punches
            ]
        }
    ), 200


@api_bp.post("/approvals/self-punch/<int:punch_id>/approve")
@require_api_auth
def api_approve_self_punch(punch_id: int):
    emp = request.api_employee
    role = _role_name(emp)

    p = BiometricPunch.query.get_or_404(punch_id)

    is_hr_admin = role in ("admin", "hr", "hr admin", "demo admin")
    is_manager = (p.employee and p.employee.reporting_manager_id == emp.id)

    if not (is_hr_admin or is_manager):
        return jsonify({"error": "Forbidden"}), 403

    if (p.approval_status or "").lower() != "pending":
        return jsonify({"error": "Already processed"}), 400

    p.approval_status = "approved"
    p.approved_by_id = emp.id
    p.approved_on = datetime.utcnow()
    p.rejection_reason = None
    db.session.commit()

    # ✅ 3F push to employee
    try:
        when = p.punch_time.date().isoformat() if p.punch_time else ""
        _push_to_employee(
            p.employee_id,
            title="Self Punch Approved",
            message=f"Your self punch ({(p.direction or '').upper()}) on {when} has been approved.",
            link="/punch",
            notif_type="success",
        )
    except Exception:
        current_app.logger.exception("Self punch push notify failed")

    try:
        from attendance_engine import recalc_attendance_for_day
        recalc_attendance_for_day(p.employee, p.punch_time.date())
    except Exception:
        current_app.logger.exception("Attendance recalc failed after API approval")

    return jsonify({"ok": True, "id": p.id, "status": p.approval_status}), 200


@api_bp.post("/approvals/self-punch/<int:punch_id>/reject")
@require_api_auth
def api_reject_self_punch(punch_id: int):
    emp = request.api_employee
    role = _role_name(emp)

    p = BiometricPunch.query.get_or_404(punch_id)

    is_hr_admin = role in ("admin", "hr", "hr admin", "demo admin")
    is_manager = (p.employee and p.employee.reporting_manager_id == emp.id)

    if not (is_hr_admin or is_manager):
        return jsonify({"error": "Forbidden"}), 403

    if (p.approval_status or "").lower() != "pending":
        return jsonify({"error": "Already processed"}), 400

    data = request.get_json(silent=True) or {}
    reason = (data.get("reason") or "").strip()

    p.approval_status = "rejected"
    p.approved_by_id = emp.id
    p.approved_on = datetime.utcnow()
    p.rejection_reason = reason[:255] if reason else None
    db.session.commit()

    return jsonify({"ok": True, "id": p.id, "status": p.approval_status}), 200


# ---------------------------------------------------------------------
# DASHBOARD (MOBILE)
# ---------------------------------------------------------------------
@api_bp.get("/dashboard")
@require_api_auth
def api_dashboard():
    emp = request.api_employee
    today = date.today()
    current_year = today.year
    current_month = today.month

    broadcasts = (
        BroadcastMessage.query.filter(
            ((BroadcastMessage.start_date == None) | (BroadcastMessage.start_date <= today)),  # noqa
            ((BroadcastMessage.end_date == None) | (BroadcastMessage.end_date >= today)),  # noqa

            # ✅ Department targeting: all OR includes employee department
            or_(
                ~BroadcastMessage.departments.any(),
                BroadcastMessage.departments.any(Department.id == emp.department_id) if emp.department_id else False
            ),

            # ✅ Role targeting: all OR includes employee role
            or_(
                ~BroadcastMessage.roles.any(),
                BroadcastMessage.roles.any(Role.id == emp.role_id) if emp.role_id else False
            ),
        )
        .order_by(BroadcastMessage.created_on.desc())
        .all()
    )

    read_ids = []
    try:
        read_ids = [r.broadcast_id for r in emp.read_broadcasts.all()]
    except Exception:
        read_ids = []

    birthdays = (
        Employee.query.filter(_status_allowed_expr())
        .filter(extract("month", Employee.dob) == current_month)
        .order_by(extract("day", Employee.dob).asc())
        .all()
    )

    new_joinees = (
        Employee.query.filter(_status_allowed_expr())
        .filter(
            extract("month", Employee.date_of_joining) == current_month,
            extract("year", Employee.date_of_joining) == current_year,
        )
        .order_by(Employee.date_of_joining.asc())
        .all()
    )

    holidays = (
        Holiday.query.filter(
            extract("month", Holiday.date) == current_month,
            extract("year", Holiday.date) == current_year,
        )
        .order_by(Holiday.date.asc())
        .all()
    )

    next_month = date(
        current_year + (1 if current_month == 12 else 0),
        1 if current_month == 12 else current_month + 1,
        1,
    )

    employees_on_leave = (
        LeaveRequest.query.join(Employee, Employee.id == LeaveRequest.employee_id)
        .filter(_status_allowed_expr())
        .filter(
            or_(LeaveRequest.status == "final_approved", LeaveRequest.status == "manager_approved"),
            or_(
                and_(LeaveRequest.start_date >= today, LeaveRequest.start_date < next_month),
                and_(LeaveRequest.end_date >= today, LeaveRequest.end_date < next_month),
                and_(LeaveRequest.start_date <= today, LeaveRequest.end_date >= today),
            ),
        )
        .order_by(LeaveRequest.start_date.asc())
        .all()
    )

    def _attachment_url(path: Optional[str]):
        if not path:
            return None
        return url_for("uploaded_document", filename=path, _external=True)

    return jsonify(
        {
            "month": current_month,
            "year": current_year,
            "broadcasts": [
                {
                    "id": b.id,
                    "title": b.title,
                    "content": b.content,
                    "created_on": b.created_on.strftime("%d %b, %Y") if b.created_on else "",
                    "is_read": (b.id in read_ids),
                    "attachment_path": getattr(b, "attachment_path", None),
                    "attachment_url": _attachment_url(getattr(b, "attachment_path", None)),
                }
                for b in broadcasts
            ],
            "birthdays": [
                {
                    "id": e.id,
                    "full_name": e.full_name,
                    "date": e.dob.strftime("%d %b") if e.dob else "",
                    "department": e.department.name if getattr(e, "department", None) else None,
                }
                for e in birthdays
            ],
            "new_joinees": [
                {
                    "id": e.id,
                    "full_name": e.full_name,
                    "date": e.date_of_joining.strftime("%d %b") if e.date_of_joining else "",
                    "department": e.department.name if getattr(e, "department", None) else None,
                }
                for e in new_joinees
            ],
            "holidays": [
                {
                    "id": h.id,
                    "name": h.name,
                    "date": h.date.strftime("%d %b") if h.date else "",
                    "type": getattr(h, "type", None),
                }
                for h in holidays
            ],
            "employees_on_leave": [
                {
                    "id": lr.id,
                    "employee_name": lr.employee.full_name if lr.employee else "Employee",
                    "department": lr.employee.department.name if lr.employee and lr.employee.department else None,
                    "start": lr.start_date.strftime("%d %b") if lr.start_date else "",
                    "end": lr.end_date.strftime("%d %b") if lr.end_date else "",
                    "status": lr.status,
                }
                for lr in employees_on_leave
            ],
        }
    ), 200


# ---------------------------------------------------------------------
# LEAVE MANAGEMENT (MOBILE)
# ---------------------------------------------------------------------
@api_bp.get("/leave/balances")
@require_api_auth
def api_leave_balances():
    emp = request.api_employee
    fy_start, fy_end = _leave_year_range()
    fy = _fy_label()

    leave_types = LeaveType.query.order_by(LeaveType.name).all()

    fld = _lb_year_field()
    q = LeaveBalance.query.filter_by(employee_id=emp.id).filter(getattr(LeaveBalance, fld) == fy).all()
    bal_map = {b.leave_type_id: float(b.balance or 0.0) for b in q}

    settings = LeaveSettings.query.first()
    max_rh = int(getattr(settings, "rh_max_per_year", 5) or 5)
    rh_in_flight = _sum_rh_in_flight_days(emp.id, fy_start, fy_end)
    rh_remaining = max(0.0, float(max_rh) - float(rh_in_flight))

    return jsonify({
        "fy_label": fy,
        "fy_start": fy_start.isoformat(),
        "fy_end": fy_end.isoformat(),
        "max_rh": max_rh,
        "rh_remaining": round(rh_remaining, 2),
        "leave_types": [
            {
                "id": lt.id,
                "name": lt.name,
                "code": lt.code,
                "allow_half_day": bool(getattr(lt, "allow_half_day", False)),
                "sandwich_applicable": bool(getattr(lt, "sandwich_applicable", False)),
                "requires_approval": bool(getattr(lt, "requires_approval", True)),
                "balance": round(float(bal_map.get(lt.id, 0.0)), 2),
            }
            for lt in leave_types
        ]
    }), 200

@api_bp.get("/leave/eligible-dates")
@require_api_auth
def api_leave_eligible_dates():
    emp = request.api_employee
    q_type = (request.args.get("type") or "").strip().lower()

    ly_start, ly_end = _leave_year_range()

    if q_type == "rh":
        all_opt = _get_optional_holiday_dates(ly_start, ly_end, employee=emp)

        in_flight = set()
        in_flight_statuses = ["pending", "manager_approved", "final_approved"]
        existing = LeaveRequest.query.filter(
            LeaveRequest.employee_id == emp.id,
            LeaveRequest.leave_type.has(code="RH"),
            LeaveRequest.start_date >= ly_start,
            LeaveRequest.end_date <= ly_end,
            LeaveRequest.status.in_(in_flight_statuses),
        ).all()

        for r in existing:
            dt = r.start_date
            while dt <= r.end_date:
                in_flight.add(dt)
                dt += timedelta(days=1)

        allowed = sorted(d for d in (all_opt - in_flight))

        items = []
        for d in allowed:
            h = _get_applicable_holiday_for(emp, d)
            items.append({
                "date": d.isoformat(),
                "name": (h.name if h else "Optional Holiday"),
            })

        return jsonify({"dates": items}), 200

    return jsonify({"dates": []}), 200


@api_bp.post("/leave/apply")
@require_api_auth
def api_leave_apply():
    emp = request.api_employee
    data = request.get_json(silent=True) or {}

    try:
        leave_type_id = int(data.get("leave_type_id"))
        start_date = datetime.strptime(data.get("start_date"), "%Y-%m-%d").date()
        end_date = datetime.strptime(data.get("end_date"), "%Y-%m-%d").date()
    except Exception:
        return jsonify({"error": "Invalid input. Required: leave_type_id, start_date, end_date (YYYY-MM-DD)."}), 400

    reason = (data.get("reason") or "").strip()
    if not reason:
        return jsonify({"error": "Reason is required"}), 400

    if end_date < start_date:
        return jsonify({"error": "End date cannot be before start date"}), 400

    half_day = bool(data.get("half_day") is True)
    half_day_session = (data.get("half_day_session") or "FH").upper()
    if half_day_session not in ("FH", "SH"):
        half_day_session = "FH"

    leave_type = LeaveType.query.get(leave_type_id)
    if not leave_type:
        return jsonify({"error": "Invalid leave_type_id"}), 400

    fy_start, fy_end = _leave_year_range()
    fy = _fy_label()

    # ---------- total days ----------
    if leave_type.code == "RH":
        if end_date != start_date:
            return jsonify({"error": "RH must be a single-day leave"}), 400

        rh_whitelist = _get_optional_holiday_dates(fy_start, fy_end, employee=emp)
        if start_date not in rh_whitelist:
            return jsonify({"error": "Only Optional (RH) holidays can be selected for RH"}), 400

        requested_units = 0.5 if half_day else 1.0
        settings = LeaveSettings.query.first()
        max_rh = int(getattr(settings, "rh_max_per_year", 5) or 5)
        rh_in_flight = _sum_rh_in_flight_days(emp.id, fy_start, fy_end)

        if (float(rh_in_flight) + float(requested_units)) > float(max_rh):
            remaining = max(0.0, float(max_rh) - float(rh_in_flight))
            return jsonify({"error": f"RH limit exceeded. Remaining RH: {remaining} day(s)."}), 400

        total_days = requested_units

    else:
        if half_day:
            if end_date != start_date:
                return jsonify({"error": "Half-day leave can only be applied for a single day"}), 400
            total_days = 0.5
        else:
            total_days = float((end_date - start_date).days + 1)

    # ---------- create request(s), split to LWP if needed ----------
    def _create_lr(lt_id: int, d1: date, d2: date, days: float):
        lr = LeaveRequest(
            employee_id=emp.id,
            leave_type_id=lt_id,
            start_date=d1,
            end_date=d2,
            total_days=float(days),
            reason=reason,
            status="pending",
            half_day=True,
            half_day_session=half_day_session,
        )
        db.session.add(lr)
        return lr

    created = []

    if leave_type.code in ("LWP", "RH"):
        created.append(_create_lr(leave_type_id, start_date, end_date, total_days))

    else:
        bal = _get_balance_row(emp.id, leave_type_id, fy)
        current_balance = float(getattr(bal, "balance", 0.0) or 0.0)

        def _get_lwp_type():
            return LeaveType.query.filter(
                func.upper(LeaveType.code).in_(["LWP", "LOP", "UL"])
            ).first()

        if total_days == 0.5:
            if current_balance >= 0.5:
                created.append(_create_lr(leave_type_id, start_date, end_date, 0.5))
            else:
                lwp = _get_lwp_type()
                if not lwp:
                    return jsonify({"error": "LWP leave type missing (code LWP/LOP/UL). Create it first."}), 500
                created.append(_create_lr(lwp.id, start_date, end_date, 0.5))

        else:
            requested = int(total_days)
            if current_balance >= requested:
                created.append(_create_lr(leave_type_id, start_date, end_date, float(requested)))
            else:
                lwp = _get_lwp_type()
                if not lwp:
                    return jsonify({"error": "LWP leave type missing (code LWP/LOP/UL). Create it first."}), 500

                paid_days = int(max(0, min(requested, int(current_balance))))
                lwp_days = int(requested - paid_days)

                if paid_days > 0:
                    partA_start = start_date
                    partA_end = start_date + timedelta(days=paid_days - 1)
                    created.append(_create_lr(leave_type_id, partA_start, partA_end, float(paid_days)))

                if lwp_days > 0:
                    partB_start = start_date + timedelta(days=paid_days)
                    partB_end = end_date
                    created.append(_create_lr(lwp.id, partB_start, partB_end, float(lwp_days)))

    db.session.commit()

    # ✅ 3C: PUSH -> Manager when leave applied (only once, not per split)
    try:
        mgr_id = getattr(emp, "reporting_manager_id", None)
        if mgr_id:
            lt_obj = LeaveType.query.get(leave_type_id)
            lt_name = (lt_obj.name if lt_obj else "Leave")
            _push_to_employee(
                mgr_id,
                title="Leave Applied",
                message=f"{emp.full_name} applied for {lt_name} ({start_date.isoformat()} to {end_date.isoformat()}).",
                link="/leave/approvals",
                notif_type="warning",
            )
    except Exception:
        current_app.logger.exception("Leave applied push notify failed")

    def _lr_json(lr: LeaveRequest):
        lt = LeaveType.query.get(lr.leave_type_id)
        return {
            "id": lr.id,
            "leave_type": {"id": lt.id, "code": lt.code, "name": lt.name} if lt else None,
            "start_date": lr.start_date.isoformat() if lr.start_date else None,
            "end_date": lr.end_date.isoformat() if lr.end_date else None,
            "total_days": float(lr.total_days or 0.0),
            "half_day": bool(getattr(lr, "half_day", False)),
            "half_day_session": getattr(lr, "half_day_session", "FH"),
            "status": lr.status,
            "reason": lr.reason,
        }

    return jsonify({
        "ok": True,
        "created": [_lr_json(x) for x in created],
        "note": "Leave submitted"
    }), 200


@api_bp.get("/leave/my-requests")
@require_api_auth
def api_leave_my_requests():
    emp = request.api_employee
    items = (
        LeaveRequest.query
        .filter_by(employee_id=emp.id)
        .order_by(LeaveRequest.applied_on.desc())
        .limit(200)
        .all()
    )

    out = []
    for lr in items:
        lt = LeaveType.query.get(lr.leave_type_id)
        out.append({
            "id": lr.id,
            "leave_type": {"id": lt.id, "code": lt.code, "name": lt.name} if lt else None,
            "start_date": lr.start_date.isoformat() if lr.start_date else None,
            "end_date": lr.end_date.isoformat() if lr.end_date else None,
            "total_days": float(lr.total_days or 0.0),
            "half_day": bool(getattr(lr, "half_day", False)),
            "half_day_session": getattr(lr, "half_day_session", "FH"),
            "status": (lr.status or "").lower(),
            "reason": lr.reason,
            "applied_on": lr.applied_on.isoformat() if getattr(lr, "applied_on", None) else None,
        })

    return jsonify({"items": out}), 200


@api_bp.post("/leave/cancel/<int:req_id>")
@require_api_auth
def api_leave_cancel(req_id: int):
    emp = request.api_employee
    lr = LeaveRequest.query.get_or_404(req_id)

    if lr.employee_id != emp.id:
        return jsonify({"error": "Forbidden"}), 403

    if (lr.status or "").lower() != "pending":
        return jsonify({"error": "Only pending requests can be cancelled"}), 400

    lr.status = "cancelled"
    db.session.commit()
    return jsonify({"ok": True, "id": lr.id, "status": lr.status}), 200


@api_bp.get("/leave/approvals/pending")
@require_api_auth
def api_leave_pending_approvals():
    emp = request.api_employee
    role = _role_name(emp)

    hr_admin_roles = {"admin", "hr", "hr admin", "demo admin"}

    if role not in hr_admin_roles:
        team_ids = [e.id for e in Employee.query.filter_by(reporting_manager_id=emp.id).all()]
        if not team_ids:
            return jsonify({"items": []}), 200

        reqs = (
            LeaveRequest.query
            .filter(LeaveRequest.employee_id.in_(team_ids))
            .filter(LeaveRequest.status == "pending")
            .order_by(LeaveRequest.applied_on.desc())
            .limit(200)
            .all()
        )
    else:
        reqs = (
            LeaveRequest.query
            .filter(LeaveRequest.status.in_(["pending", "manager_approved"]))
            .order_by(LeaveRequest.applied_on.desc())
            .limit(200)
            .all()
        )

    def _stage_info(status: str):
        st = (status or "").lower().strip()
        if st == "pending":
            return ("manager", "Pending with Manager")
        if st == "manager_approved":
            return ("hr", "Pending with HR")
        return ("other", st)

    items = []
    for r in reqs:
        lt = LeaveType.query.get(r.leave_type_id)
        e = Employee.query.get(r.employee_id)

        stage, stage_label = _stage_info(r.status)

        items.append({
            "id": r.id,
            "employee": {
                "id": e.id,
                "employee_code": e.employee_code,
                "full_name": e.full_name,
                "department": e.department.name if getattr(e, "department", None) else None,
            } if e else None,
            "leave_type": {"id": lt.id, "code": lt.code, "name": lt.name} if lt else None,
            "start_date": r.start_date.isoformat() if r.start_date else None,
            "end_date": r.end_date.isoformat() if r.end_date else None,
            "total_days": float(r.total_days or 0.0),
            "status": (r.status or "").lower(),
            "approval_stage": stage,
            "stage_label": stage_label,
            "reason": r.reason,
        })

    return jsonify({"items": items}), 200


@api_bp.post("/leave/approvals/<int:req_id>/approve")
@require_api_auth
def api_leave_approve(req_id: int):
    emp = request.api_employee
    role = _role_name(emp)

    hr_admin_roles = {"admin", "hr", "hr admin", "demo admin"}

    lr = LeaveRequest.query.get_or_404(req_id)

    data = request.get_json(silent=True) or {}
    remark = (data.get("remark") or "").strip()

    # ---------------- MANAGER APPROVAL ----------------
    if role not in hr_admin_roles:
        if (lr.status or "").lower() != "pending":
            return jsonify({"error": "Not eligible"}), 400

        employee = Employee.query.get(lr.employee_id)
        if not employee or employee.reporting_manager_id != emp.id:
            return jsonify({"error": "Forbidden"}), 403

        lr.status = "manager_approved"

        if hasattr(lr, "approved_by_manager_id"):
            lr.approved_by_manager_id = emp.id

        db.session.add(LeaveApprovalLog(
            leave_request_id=lr.id,
            approver_id=emp.id,
            approver_role="manager",
            status="approved",
            remark=remark[:255],
        ))
        db.session.commit()

        # ✅ 3D: PUSH to employee: approved by manager
        try:
            _push_to_employee(
                lr.employee_id,
                title="Leave Approved (Manager)",
                message=f"Your leave request ({lr.start_date.isoformat()} to {lr.end_date.isoformat()}) is approved by Manager.",
                link="/leave/my",
                notif_type="success",
            )
        except Exception:
            current_app.logger.exception("Leave manager-approved push notify failed")

        return jsonify({"ok": True, "id": lr.id, "status": lr.status}), 200

    # ---------------- HR / ADMIN FINAL APPROVAL ----------------
    st = (lr.status or "").lower().strip()
    if st not in ("pending", "manager_approved"):
        return jsonify({"error": "Not eligible"}), 400

    if st == "pending":
        lr.status = "manager_approved"
        if hasattr(lr, "approved_by_manager_id"):
            lr.approved_by_manager_id = emp.id

        db.session.add(LeaveApprovalLog(
            leave_request_id=lr.id,
            approver_id=emp.id,
            approver_role="manager",
            status="approved",
            remark=("Auto-approved by HR/Admin (direct approval). " + remark)[:255],
        ))
        db.session.flush()

    lt = LeaveType.query.get(lr.leave_type_id)
    fy = _fy_label()

    if lt and (lt.code or "").upper() not in ("RH", "LWP", "LOP", "UL"):
        bal = _get_balance_row(lr.employee_id, lr.leave_type_id, fy)
        if not bal:
            fld = _lb_year_field()
            bal = LeaveBalance(employee_id=lr.employee_id, leave_type_id=lr.leave_type_id, balance=0.0)
            setattr(bal, fld, fy)
            db.session.add(bal)
            db.session.flush()

        need = 0.5 if (bool(getattr(lr, "half_day", False)) or float(lr.total_days or 0.0) == 0.5) else float(lr.total_days or 0.0)
        if float(bal.balance or 0.0) < need:
            db.session.rollback()
            return jsonify({"error": f"Insufficient balance. Available: {bal.balance}"}), 400

        bal.balance = float(bal.balance or 0.0) - need

    lr.status = "final_approved"
    db.session.add(LeaveApprovalLog(
        leave_request_id=lr.id,
        approver_id=emp.id,
        approver_role="hr",
        status="approved",
        remark=remark[:255],
    ))
    db.session.commit()

    # ✅ 3E: PUSH to employee: approved by HR
    try:
        _push_to_employee(
            lr.employee_id,
            title="Leave Approved (HR)",
            message=f"Your leave request ({lr.start_date.isoformat()} to {lr.end_date.isoformat()}) is approved by HR.",
            link="/leave/my",
            notif_type="success",
        )
    except Exception:
        current_app.logger.exception("Leave HR-approved push notify failed")

    try:
        sync_leave_to_attendance(
            db,
            lr.employee_id,
            lr.start_date,
            lr.end_date,
            lt,  # pass full LeaveType
            half_day=bool(getattr(lr, "half_day", False) or float(lr.total_days or 0.0) == 0.5),
            half_day_session=getattr(lr, "half_day_session", "FH"),
        )
        db.session.commit()
    except Exception:
        current_app.logger.exception("Attendance sync failed after API HR approval")

    return jsonify({"ok": True, "id": lr.id, "status": lr.status}), 200


# ---------------------------------------------------------------------
# PAYSLIPS (MOBILE)
# ---------------------------------------------------------------------
def _is_hr_admin_emp(emp: Employee) -> bool:
    role = (emp.role.name if emp.role else "").strip().lower()
    return role in ("hr", "admin", "hr admin", "demo admin")


@api_bp.get("/payslips")
@require_api_auth
def api_payslip_list():
    emp = request.api_employee
    is_hr_admin = _is_hr_admin_emp(emp)

    scope = (request.args.get("scope") or "").strip().lower()
    employee_id = request.args.get("employee_id", type=int)

    q = Payroll.query.options(joinedload(Payroll.employee)).order_by(Payroll.month.desc())

    if is_hr_admin and employee_id:
        q = q.filter(Payroll.employee_id == employee_id)
    elif is_hr_admin and scope == "all":
        pass
    else:
        q = q.filter(Payroll.employee_id == emp.id)

    rows = q.all()

    data = []
    for p in rows:
        payroll_emp = p.employee

        feedback_required_block = False
        if p.employee_id == emp.id and (not is_hr_admin):
            feedback_required_block = not _has_filled_feedback(p.employee_id, p.month)

        data.append({
            "employee_id": p.employee_id,
            "employee_name": payroll_emp.full_name if payroll_emp else "",
            "month": p.month,
            "gross_pay": float(p.gross_pay or 0.0),
            "deductions": float(p.deductions or 0.0),
            "net_pay": float(p.net_pay or 0.0),
            "paid_days": float((p.present_days or 0) + (p.approved_leaves or 0) + (p.holidays or 0) + (p.weekends or 0)),
            "feedback_pending": bool(feedback_required_block),
            "pdf_url": f"/api/payslips/{p.month}/pdf",
        })

    return jsonify({"success": True, "data": data}), 200


@api_bp.get("/payslips/<string:month>/pdf")
@require_api_auth
def api_payslip_pdf(month: str):
    emp = request.api_employee
    is_hr_admin = _is_hr_admin_emp(emp)

    try:
        month = _normalize_month(month)
    except Exception:
        return jsonify({"success": False, "message": "Invalid month format. Use YYYY-MM"}), 400

    employee_id = request.args.get("employee_id", type=int) or emp.id
    is_self = (employee_id == emp.id)

    if not is_hr_admin and not is_self:
        return jsonify({"success": False, "message": "Not authorized"}), 403

    if is_self and not is_hr_admin:
        if not _has_filled_feedback(employee_id, month):
            return jsonify({
                "success": False,
                "message": "Feedback required before viewing salary slip.",
                "code": "FEEDBACK_REQUIRED",
                "month": month
            }), 403

    exists = Payroll.query.filter_by(employee_id=employee_id, month=month).first()
    if not exists:
        return jsonify({"success": False, "message": "No payroll found for this month"}), 404

    return _salary_slip_pdf_response(employee_id=employee_id, month=month, disposition="inline")


# ---------------------------------------------------------------------
# NOTIFICATIONS (MOBILE) - In-app
# ---------------------------------------------------------------------
@api_bp.get("/notifications")
@require_api_auth
def api_notifications_list():
    emp = request.api_employee

    only_unread = (request.args.get("only_unread") or "0").strip() == "1"
    limit = request.args.get("limit", type=int) or 50
    limit = max(1, min(limit, 200))

    q = _notif_base_query(emp).order_by(desc(Notification.created_at))
    if only_unread:
        q = q.filter(Notification.is_read.is_(False))

    rows = q.limit(limit).all()

    unread_count = (
        _notif_base_query(emp)
        .filter(Notification.is_read.is_(False))
        .count()
    )

    items = []
    for n in rows:
        items.append({
            "id": n.id,
            "title": n.title,
            "message": n.message,
            "type": n.type,
            "link": _canonicalize_link(n.link),
            "is_read": bool(n.is_read),
            "created_at": n.created_at.strftime("%Y-%m-%d %H:%M") if n.created_at else "",
        })

    return jsonify({"items": items, "unread_count": unread_count}), 200


@api_bp.get("/notifications/count")
@require_api_auth
def api_notifications_count():
    emp = request.api_employee
    c = (
        _notif_base_query(emp)
        .filter(Notification.is_read.is_(False))
        .count()
    )
    return jsonify({"count": int(c)}), 200


@api_bp.post("/notifications/<int:notif_id>/read")
@require_api_auth
def api_notifications_mark_read(notif_id: int):
    emp = request.api_employee
    n = Notification.query.get_or_404(notif_id)

    if not (n.employee_id == emp.id or n.employee_id is None):
        return jsonify({"error": "Forbidden"}), 403

    if not n.is_read:
        n.is_read = True
        db.session.commit()

    return jsonify({"ok": True, "id": n.id}), 200


@api_bp.post("/notifications/read-all")
@require_api_auth
def api_notifications_mark_all_read():
    emp = request.api_employee
    q = (
        _notif_base_query(emp)
        .filter(Notification.is_read.is_(False))
    )
    updated = q.update({Notification.is_read: True}, synchronize_session=False)
    db.session.commit()
    return jsonify({"ok": True, "updated": int(updated or 0)}), 200


@api_bp.post("/devices/register")
@require_api_auth
def api_register_device():
    emp = request.api_employee
    data = request.get_json(silent=True) or {}

    token = (data.get("token") or "").strip()
    platform = (data.get("platform") or "unknown").strip().lower()

    if not token:
        return jsonify({"error": "token required"}), 400

    row = DeviceToken.query.filter_by(token=token).first()
    if row:
        row.employee_id = emp.id
        row.platform = platform
        row.is_active = True
        row.last_seen_at = datetime.utcnow()
    else:
        row = DeviceToken(
            employee_id=emp.id,
            token=token,
            platform=platform,
            is_active=True,
            last_seen_at=datetime.utcnow(),
        )
        db.session.add(row)

    db.session.commit()
    return jsonify({"ok": True}), 200


@api_bp.post("/devices/unregister")
@require_api_auth
def api_unregister_device():
    data = request.get_json(silent=True) or {}
    token = (data.get("token") or "").strip()
    if not token:
        return jsonify({"error": "token required"}), 400

    row = DeviceToken.query.filter_by(token=token).first()
    if row:
        row.is_active = False
        row.last_seen_at = datetime.utcnow()
        db.session.commit()

    return jsonify({"ok": True}), 200

from utils.push import send_push

@api_bp.get("/test-push")
def test_push():
    token = "fxNXw5JTT1mCEyXUHEH9xh:APA91bEF-qjd_rCASVfqJ3XzS1qJp8vV_DCf2dtWPT0OjN3GSSkTEDFca95XR0nDZTxYyC_weSIudWMtRUY9l2egDsu51CR46H5rUFmRT4BZp-EnXkRFJfY"

    result = send_push(
        [token],
        title="Test Push",
        body="Hello from backend 🚀",
        data={"link": "/notifications", "type": "info"},
    )

    return jsonify({"status": "sent", "result": result}), 200