# cli/attendance_cli.py

import json
from datetime import datetime, timedelta, date
from typing import Optional

import click
import requests
from flask import current_app
from flask.cli import with_appcontext
from sqlalchemy import and_, func

from extensions import db
from models import (
    Attendance,
    Employee,
    PublicHoliday,   # if you've fully moved to Holiday, swap this import
    LeaveRequest,
    DesktimeLog,
    WeekendSettings,
    SyncLog,
)
from common_utils import get_setting
from routes.attendance_engine import recalc_attendance_for_day
from routes.biometric import sync_biometric_for_date

# ---------------------------------------------------------------------
# DeskTime helpers
# ---------------------------------------------------------------------

DESKTIME_API_KEY = "f9da2cfcf20ff9508002b5a74aa0ff59"
DESKTIME_URL = "https://desktime.com/api/v2/json/employee"


def _load_desktime_config() -> dict:
    """
    Load settings from SystemSetting.key == 'desktime_settings'.

    Handles both:
      - value/json_value as JSON string
      - or already-parsed dict (if get_setting ever supports that)
    """
    raw = get_setting("desktime_settings", default=None)

    # If it's already a dict, use it
    if isinstance(raw, dict):
        return raw

    # If it's a JSON string, parse it
    if isinstance(raw, str) and raw.strip():
        try:
            parsed = json.loads(raw)
            if isinstance(parsed, dict):
                return parsed
        except Exception:
            current_app.logger.exception(
                "Failed to parse desktime_settings JSON; raw=%r", raw
            )

    # Fallback: empty config
    return {}

def _is_desktime_enabled() -> bool:
    """
    Check whether DeskTime integration is enabled.

    Reads desktime_settings.enabled and interprets common truthy values.
    """
    cfg = _load_desktime_config()
    enabled = cfg.get("enabled")

    # If it's already a proper boolean
    if isinstance(enabled, bool):
        return enabled

    # Strings: "1", "true", "yes", "on", "enabled" → True
    if isinstance(enabled, str):
        return enabled.strip().lower() in ("1", "true", "yes", "on", "enabled")

    # Numbers: non-zero → True
    if isinstance(enabled, (int, float)):
        return enabled != 0

    # Fallback: disabled
    return False


def _get_desktime_api_key() -> Optional[str]:
    """
    Get DeskTime API key from desktime_settings.api_key,
    fallback to DESKTIME_API_KEY if missing.
    """
    cfg = _load_desktime_config()
    api_key = cfg.get("api_key")
    if api_key:
        return str(api_key).strip() or None
    return DESKTIME_API_KEY


def fetch_and_store_desktime(employee: Employee, target_date: date) -> float:
    """
    Fetch a single day's DeskTime data for one employee and store in DesktimeLog.
    Returns productive_hours (float, in hours).
    """
    api_key = _get_desktime_api_key()
    if not api_key:
        current_app.logger.error("DeskTime API key missing; cannot sync.")
        return 0.0

    formatted_date = target_date.strftime("%Y-%m-%d")
    url = f"{DESKTIME_URL}?apiKey={api_key}&id={employee.desktime_id}&date={formatted_date}"

    try:
        resp = requests.get(url, timeout=20)
    except Exception as e:  # noqa: BLE001
        current_app.logger.error(
            "DeskTime request error for %s on %s: %s",
            employee.employee_code,
            formatted_date,
            e,
        )
        return 0.0

    if resp.status_code != 200:
        current_app.logger.warning(
            "DeskTime API failed for %s on %s: HTTP %s",
            employee.desktime_id,
            formatted_date,
            resp.status_code,
        )
        return 0.0

    try:
        data = resp.json()
    except Exception as e:  # noqa: BLE001
        current_app.logger.error("DeskTime JSON parse failed: %s", e)
        return 0.0

    productive_seconds = data.get("productiveTime", 0) or 0
    first_login = data.get("arrived")
    last_logout = data.get("left")

    log = DesktimeLog.query.filter_by(employee_id=employee.id, date=target_date).first()
    if not log:
        log = DesktimeLog(employee_id=employee.id, date=target_date)

    def _parse_ts(ts: Optional[str]):
        if not ts:
            return None
        try:
            return datetime.strptime(ts, "%Y-%m-%d %H:%M:%S").time()
        except Exception:
            return None

    log.first_activity = _parse_ts(first_login)
    log.last_activity = _parse_ts(last_logout)
    log.productive_hours = productive_seconds / 3600.0 if productive_seconds else 0.0
    log.raw_data = json.dumps(data)

    db.session.add(log)
    db.session.flush()  # not strictly required, but keeps things consistent
    return log.productive_hours or 0.0


# ---------------------------------------------------------------------
# Legacy "process-attendance" (kept for backward compat)
# ---------------------------------------------------------------------


@click.command("process-attendance")
@with_appcontext
def process_attendance():
    """
    Legacy FY-wide attendance processor.

    NOTE: for new logic you should prefer the attendance-engine
    based CLI: `flask attendance-run --from YYYY-MM-DD --to YYYY-MM-DD`.
    """
    today = date.today()
    fy_start = today.replace(month=4, day=1) if today.month >= 4 else date(
        today.year - 1, 4, 1
    )
    fy_end = fy_start.replace(year=fy_start.year + 1) - timedelta(days=1)

    employees = Employee.query.filter_by(status="active").all()
    weekend_settings = WeekendSettings.query.order_by(
        WeekendSettings.updated_on.desc()
    ).first()

    def is_saturday_off(day: date) -> bool:
        if day.weekday() != 5:
            return False
        week_number = (day.day - 1) // 7 + 1
        attr = f"{['first', 'second', 'third', 'fourth', 'fifth'][week_number - 1]}_saturday"
        return getattr(weekend_settings, attr) == "off"

    def is_sunday_off(day: date) -> bool:
        return day.weekday() == 6 and weekend_settings.sunday == "off"

    for emp in employees:
        cur = fy_start
        while cur <= fy_end:
            if cur > today:
                break  # ⛔ Skip future dates

            is_weekend = is_saturday_off(cur) or is_sunday_off(cur)
            is_holiday = (
                PublicHoliday.query.filter_by(date=cur).first() is not None
            )  # swap to Holiday if needed
            is_leave = (
                LeaveRequest.query.filter_by(
                    employee_id=emp.id, status="final_approved"
                )
                .filter(
                    LeaveRequest.start_date <= cur,
                    LeaveRequest.end_date >= cur,
                )
                .first()
                is not None
            )

            existing = Attendance.query.filter_by(
                employee_id=emp.id, date=cur
            ).first()

            # If existing row is already Desktime and you don't want to reprocess,
            # keep this short-circuit:
            if existing and existing.source == "Desktime":
                cur += timedelta(days=1)
                continue

            hours = 0.0
            source = "System"

            if is_holiday:
                remark = "Holiday"
                source = "Holiday"
            elif is_leave:
                remark = "Leave"
                source = "Leave"
            elif is_weekend:
                remark = "Holiday"
                source = "Weekend"
            else:
                if (
                    existing
                    and existing.check_in_time
                    and existing.check_out_time
                ):
                    diff = datetime.combine(
                        date.min, existing.check_out_time
                    ) - datetime.combine(date.min, existing.check_in_time)
                    hours = round(diff.total_seconds() / 3600.0, 2)
                    source = "Punch"
                elif emp.desktime_id:
                    hours = fetch_and_store_desktime(emp, cur)
                    if hours > 0:
                        source = "Desktime"

                if hours >= 7:
                    remark = "Present"
                elif 3.5 <= hours < 7:
                    remark = "Half Day"
                elif 0 < hours < 3.5:
                    remark = "LWP"
                else:
                    remark = "LWP"

            if existing:
                existing.working_hours = hours
                existing.remarks = remark
                existing.source = source
            else:
                db.session.add(
                    Attendance(
                        employee_id=emp.id,
                        date=cur,
                        check_in_time=None,
                        check_out_time=None,
                        working_hours=hours,
                        remarks=remark,
                        source=source,
                    )
                )

            cur += timedelta(days=1)

    db.session.commit()
    click.echo("✅ Legacy attendance processing complete.")


# ---------------------------------------------------------------------
# Daily DeskTime sync (cron helper)
# ---------------------------------------------------------------------


def run_daily_desktime_sync(target_date: Optional[date] = None) -> SyncLog:
    """
    Fetch DeskTime data for all employees with desktime_id for a single day,
    and record a SyncLog row with status and summary payload.
    """
    if not _is_desktime_enabled():
        current_app.logger.info(
            "DeskTime sync skipped – integration disabled in settings."
        )
        # Optional: record a skipped SyncLog
        log_skipped = SyncLog(
            started_at=datetime.utcnow(),
            ended_at=datetime.utcnow(),
            status="skipped",
            message="DeskTime integration disabled in settings.",
        )
        db.session.add(log_skipped)
        db.session.commit()
        return log_skipped

    if target_date is None:
        # run for yesterday so the day is fully complete
        target_date = date.today() - timedelta(days=1)

    log_entry = SyncLog(
        started_at=datetime.utcnow(),
        status="running",
        message=f"Starting daily DeskTime sync for {target_date.isoformat()}",
    )
    db.session.add(log_entry)
    db.session.commit()

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

    employees = (
        Employee.query.filter(
            Employee.desktime_id.isnot(None),
            func.lower(Employee.status).in_([s.lower() for s in allowed_statuses]),
        ).all()
    )

    total = len(employees)
    success = 0
    failed = 0
    errors = []

    for emp in employees:
        try:
            # clear existing entries for that day to keep data idempotent
            Attendance.query.filter_by(
                employee_id=emp.id, date=target_date
            ).delete()
            DesktimeLog.query.filter_by(
                employee_id=emp.id, date=target_date
            ).delete()
            db.session.commit()

            fetch_and_store_desktime(emp, target_date)
            success += 1
        except Exception as e:  # noqa: BLE001
            db.session.rollback()
            failed += 1
            errors.append(f"{emp.employee_code} - {emp.full_name}: {str(e)[:200]}")

    log_entry.ended_at = datetime.utcnow()
    if failed == 0 and success > 0:
        status = "success"
    elif success > 0:
        status = "partial"
    else:
        status = "failed"

    payload = {
        "target_date": target_date.isoformat(),
        "total_employees": total,
        "success": success,
        "failed": failed,
        "errors": errors,
    }

    log_entry.status = status
    log_entry.message = json.dumps(payload, ensure_ascii=False)
    db.session.commit()

    return log_entry


# ---------------------------------------------------------------------
# Range helpers – NO dependency on services.*
# ---------------------------------------------------------------------


def _sync_desktime_for_range(from_date: str, to_date: str, employees: str):
    """
    Sync DeskTime logs into DesktimeLog for a date range.

    - employees='all' → all employees with desktime_id
    - employees='<code>' → single employee_code
    """
    start = date.fromisoformat(from_date)
    end = date.fromisoformat(to_date)
    today = date.today()

    if start > end:
        raise click.ClickException("from date cannot be after to date")

    qs = Employee.query.filter(Employee.desktime_id.isnot(None))
    if employees != "all":
        qs = qs.filter(Employee.employee_code == employees)

    emps = qs.all()
    if not emps:
        click.echo("No employees found with desktime_id for given filter.")
        return

    click.echo(f"  → Employees with DeskTime ID: {len(emps)}")

    total_calls = 0
    for d in (start + timedelta(n) for n in range((end - start).days + 1)):
        if d > today:
            continue

        click.echo(f"    • Date {d.isoformat()}")
        for emp in emps:
            # respect DOJ / exit if present
            if emp.date_of_joining and d < emp.date_of_joining:
                continue
            if getattr(emp, "exit_date", None) and d > emp.exit_date:
                continue

            try:
                fetch_and_store_desktime(emp, d)
                total_calls += 1
            except Exception as e:  # noqa: BLE001
                click.echo(f"      ⚠ {emp.employee_code} {emp.full_name}: {e}")

    db.session.commit()
    click.echo(f"  ✓ DeskTime sync done. API calls attempted: {total_calls}")


def _sync_biometric_for_range(from_date: str, to_date: str, employees: str):
    """
    Range sync for biometric punches using your existing sync_biometric_for_date.

    NOTE:
    - Currently ignores the `employees` filter and syncs punches for all
      employees defined inside sync_biometric_for_date (device-level).
    """
    start = date.fromisoformat(from_date)
    end = date.fromisoformat(to_date)
    today = date.today()

    if start > end:
        raise click.ClickException("from date cannot be after to date")

    if employees != "all":
        click.echo(
            "  ⚠ employees filter is not supported for biometric-sync yet; syncing all punches per day."
        )

    for d in (start + timedelta(n) for n in range((end - start).days + 1)):
        if d > today:
            continue

        click.echo(f"    • Biometric sync for {d.isoformat()}")
        try:
            sync_biometric_for_date(d, source_name="default")
        except Exception as e:  # noqa: BLE001
            click.echo(f"      ❌ Biometric sync failed for {d}: {e}")

    db.session.commit()
    click.echo("  ✓ Biometric range sync completed.")


# ---------------------------------------------------------------------
# CLI registration
# ---------------------------------------------------------------------


def register_cli(app):
    """
    Call this from app.py to register CLI commands:

        from cli.attendance_cli import register_cli
        register_cli(app)
    """

    @app.cli.command("desktime-sync")
    @click.option("--from", "from_date", required=True, help="Start date YYYY-MM-DD")
    @click.option("--to", "to_date", required=True, help="End date YYYY-MM-DD")
    @click.option(
        "--employees",
        default="all",
        help="Either 'all' or a single employee_code.",
    )
    def desktime_sync_cmd(from_date, to_date, employees):
        """Sync DeskTime logs for the given date range."""
        click.echo(f"▶ DeskTime sync: {from_date} → {to_date}, employees={employees}")
        _sync_desktime_for_range(from_date, to_date, employees)
        click.echo("✓ DeskTime sync finished.")

    @app.cli.command("biometric-sync")
    @click.option("--from", "from_date", required=True, help="Start date YYYY-MM-DD")
    @click.option("--to", "to_date", required=True, help="End date YYYY-MM-DD")
    @click.option(
        "--employees",
        default="all",
        help="Either 'all' or a single employee_code.",
    )
    def biometric_sync_cmd(from_date, to_date, employees):
        """Sync biometric punches for the given date range."""
        click.echo(
            f"▶ Biometric sync: {from_date} → {to_date}, employees={employees}"
        )
        _sync_biometric_for_range(from_date, to_date, employees)
        click.echo("✓ Biometric sync finished.")

    @app.cli.command("attendance-run")
    @click.option("--from", "from_date", required=True, help="Start date YYYY-MM-DD")
    @click.option("--to", "to_date", required=True, help="End date YYYY-MM-DD")
    @click.option(
        "--employees",
        default="all",
        help="Either 'all' or a single employee_code.",
    )
    def attendance_run_cmd(from_date, to_date, employees):
        """
        Recalculate attendance using unified engine (attendance_engine)
        for the given date range.
        """
        start = date.fromisoformat(from_date)
        end = date.fromisoformat(to_date)

        qs = Employee.query
        if employees != "all":
            qs = qs.filter(Employee.employee_code == employees)

        emps = qs.all()
        click.echo(
            f"▶ Attendance engine for {len(emps)} employees: {from_date} → {to_date}"
        )

        for emp in emps:
            d = start
            while d <= end:
                recalc_attendance_for_day(emp, d)
                d += timedelta(days=1)

        db.session.commit()
        click.echo("✓ Attendance recalculation completed.")

    @app.cli.command("daily-attendance")
    @click.option(
        "--date",
        "target_date",
        type=click.DateTime(formats=["%Y-%m-%d"]),
        required=False,
        help="Date to process in YYYY-MM-DD (defaults to yesterday).",
    )
    @with_appcontext
    def daily_attendance_command(target_date=None):
        """
        Sync biometric + DeskTime and recalc attendance for all employees
        for a single target date.
        """
        # -------------- resolve target date --------------
        if target_date is None:
            target_date = date.today() - timedelta(days=1)
        else:
            target_date = target_date.date()

        click.echo(f"▶ Processing daily attendance for {target_date} …")

        # -------------- 1) Biometric sync ----------------
        try:
            sync_biometric_for_date(target_date, source_name="default")
            click.echo("  ✓ Biometric sync done")
        except Exception as e:  # noqa: BLE001
            click.echo(f"  ⚠ Biometric sync failed: {e}")

        # -------------- 2) DeskTime sync -----------------
        employees = (
            Employee.query.filter(Employee.desktime_id.isnot(None))
            .order_by(Employee.full_name)
            .all()
        )

        for emp in employees:
            if emp.date_of_joining and target_date < emp.date_of_joining:
                continue
            if getattr(emp, "exit_date", None) and target_date > emp.exit_date:
                continue

            try:
                fetch_and_store_desktime(emp, target_date)
            except Exception as e:  # noqa: BLE001
                click.echo(
                    f"  ⚠ DeskTime fetch failed for {emp.full_name} ({emp.employee_code}): {e}"
                )

        db.session.commit()
        click.echo("  ✓ DeskTime sync done")

        # -------------- 3) Recalc attendance ------------
        processed = 0
        all_emps = Employee.query.order_by(Employee.full_name).all()
        for emp in all_emps:
            if emp.date_of_joining and target_date < emp.date_of_joining:
                continue
            if getattr(emp, "exit_date", None) and target_date > emp.exit_date:
                continue

            try:
                recalc_attendance_for_day(emp, target_date)
                processed += 1
            except Exception as e:  # noqa: BLE001
                click.echo(
                    f"  ⚠ recalc_attendance_for_day failed for {emp.full_name}: {e}"
                )

        db.session.commit()
        click.echo(
            f"  ✓ Attendance recalculated for {processed} employees on {target_date}"
        )