# routes/biometric.py
from __future__ import annotations

import json
from datetime import datetime, date, time, timedelta

import requests
from flask import current_app
from flask_login import current_user

from extensions import db
from models import Employee, BiometricPunch, BiometricSyncRun, SystemSetting, BiometricEmployeeMap


# --- Settings helper (same idea as in admin.brand_config) -------------------

def get_json_setting(key: str, default):
    setting = SystemSetting.query.filter_by(key=key).first()
    if setting and getattr(setting, "json_value", None):
        try:
            return json.loads(setting.json_value)
        except Exception:
            current_app.logger.exception("Invalid JSON for setting %s", key)
            return default
    return default


def _resolve_list(data, path: str):
    """Resolve nested list by dot path like 'Data' or 'data.records'."""
    if not path:
        return data if isinstance(data, list) else []

    parts = [p for p in path.split('.') if p]
    cur = data
    for p in parts:
        if isinstance(cur, dict):
            cur = cur.get(p)
        else:
            return []
    return cur if isinstance(cur, list) else []


# ---------------------------------------------------------------------------
# PUBLIC: sync a single date
# ---------------------------------------------------------------------------

def sync_biometric_for_date(target_date: date, source_name: str = "default") -> BiometricSyncRun:
    """
    Fetch biometric data for a single date based on biometric_settings
    and store in BiometricPunch, grouped under BiometricSyncRun.

    Returns the BiometricSyncRun row.
    """
    run = BiometricSyncRun(
        sync_date=target_date,
        source_type="api",  # will be overwritten if DB
        source_name=source_name,
        status="running",
        started_at=datetime.utcnow(),
    )
    try:
        # Optional: who triggered the sync (if logged-in context exists)
        try:
            if current_user and getattr(current_user, "id", None):
                run.triggered_by_id = current_user.id
        except Exception:
            pass

        db.session.add(run)
        db.session.flush()  # ensure run.id

        cfg = get_json_setting("biometric_settings", {})
        if not cfg or not cfg.get("enabled"):
            run.status = "skipped"
            run.message = "Biometric integration disabled."
            run.ended_at = datetime.utcnow()
            db.session.commit()
            return run

        source_type = (cfg.get("source_type") or "api").lower()
        run.source_type = source_type

        if source_type == "api":
            count = _sync_biometric_from_api(cfg, target_date, run)
        elif source_type == "db":
            count = _sync_biometric_from_db(cfg, target_date, run)
        else:
            raise ValueError(f"Unsupported biometric source_type: {source_type}")

        run.status = "success"
        run.punches_saved = count

        # NOTE: _sync_biometric_from_api may append extra notes to run.message
        base_msg = f"Biometric sync OK, punches stored: {count}"
        run.message = (run.message + " | " + base_msg) if run.message else base_msg

        run.ended_at = datetime.utcnow()
        db.session.commit()
        return run

    except Exception as e:
        current_app.logger.exception("Biometric sync failed for %s", target_date)
        db.session.rollback()
        run.status = "error"
        run.message = f"Error: {e}"
        run.ended_at = datetime.utcnow()
        db.session.add(run)
        db.session.commit()
        return run


# ---------------------------------------------------------------------------
# API-based sync (your current case)
# ---------------------------------------------------------------------------

def _sync_biometric_from_api(cfg: dict, target_date: date, run: BiometricSyncRun) -> int:
    api_cfg = cfg.get("api", {}) or {}

    base_url = (api_cfg.get("base_url") or "").strip()
    http_method = (api_cfg.get("api_http_method") or api_cfg.get("http_method") or "GET").upper()
    req_tmpl = (api_cfg.get("api_request_template") or api_cfg.get("request_template") or "").strip()

    if not base_url:
        raise ValueError("Biometric API base_url is not configured.")

    # --- build URL ---------------------------------------------------------
    date_str = target_date.strftime("%Y-%m-%d")
    query_part = req_tmpl.format(date=date_str) if req_tmpl else ""
    if query_part and not query_part.startswith(("?", "&")):
        query_part = "?" + query_part
    url = base_url.rstrip("/") + query_part

    # --- headers -----------------------------------------------------------
    headers_json = api_cfg.get("api_headers_json") or api_cfg.get("headers_json") or ""
    try:
        headers = json.loads(headers_json) if headers_json.strip() else {}
    except Exception:
        headers = {}
        current_app.logger.warning("Invalid biometric API headers_json; ignoring.")

    current_app.logger.info("Biometric API call: %s %s", http_method, url)

    if http_method == "POST":
        resp = requests.post(url, headers=headers, timeout=30)
    else:
        resp = requests.get(url, headers=headers, timeout=30)

    resp.raise_for_status()
    payload = resp.json()

    # --- find list of rows -------------------------------------------------
    list_key = api_cfg.get("api_response_list_key") or api_cfg.get("response_list_key") or "Data"
    rows = _resolve_list(payload, list_key)

    if not isinstance(rows, list):
        current_app.logger.warning("Biometric API list resolved to non-list.")
        return 0

    # --- mapping config ----------------------------------------------------
    def pick(*keys, default=None):
        for k in keys:
            v = api_cfg.get(k)
            if v:
                return v
        return default

    map_emp_code   = pick("api_map_employee_code", "map_employee_code") or "Employee_Code"
    map_punch_time = pick("api_map_punch_time", "map_punch_time")  # single punch field
    map_in_time    = pick("api_map_in_time", "map_in_time")
    map_out_time   = pick("api_map_out_time", "map_out_time")
    map_direction  = pick("api_map_direction", "map_direction")
    map_device     = pick("api_map_device_code", "map_device_code") or "Card_Number"
    time_fmt       = pick("api_time_format", "time_format") or "%Y-%m-%dT%H:%M:%S"

    # Allow "in_time,out_time" in a single punch field config
    if (not map_in_time and not map_out_time) and map_punch_time and "," in map_punch_time:
        parts = [p.strip() for p in map_punch_time.split(",") if p.strip()]
        if len(parts) == 2:
            map_in_time, map_out_time = parts
            map_punch_time = None  # switch to separate IN/OUT mode

    # Decide mode:
    #  - if explicit in/out fields configured -> use them
    #  - else if single punch field configured -> "single" mode
    #  - else fall back to vendor-style PunchIn / PunchOut
    if map_in_time or map_out_time:
        mode = "separate"
        map_in_time = map_in_time or "PunchIn"
        map_out_time = map_out_time or "PunchOut"
    elif map_punch_time:
        mode = "single"
    else:
        mode = "separate"
        map_in_time = "PunchIn"
        map_out_time = "PunchOut"

    current_app.logger.info(
        "Biometric API mode=%s emp=%s in=%s out=%s punch=%s direction=%s fmt=%s",
        mode, map_emp_code, map_in_time, map_out_time, map_punch_time, map_direction, time_fmt
    )

    # --- clear existing punches for that date & source ---------------------
    start_dt = datetime.combine(target_date, time.min)
    end_dt = datetime.combine(target_date + timedelta(days=1), time.min)

    BiometricPunch.query.filter(
        BiometricPunch.punch_time >= start_dt,
        BiometricPunch.punch_time < end_dt,
        BiometricPunch.source_name == run.source_name,
        BiometricPunch.source_type == "api",
    ).delete(synchronize_session=False)
    db.session.flush()

    total_saved = 0
    emp_cache: dict[str, int | None] = {}

    # track unmapped codes (so you can configure BiometricEmployeeMap)
    unmapped_count = 0
    unmapped_codes: set[str] = set()

    def resolve_employee_id(ext_code: str):
        if ext_code in emp_cache:
            return emp_cache[ext_code]
        employee_id = None
        mapping = BiometricEmployeeMap.query.filter_by(
            external_code=ext_code,
            source_name=run.source_name,
            is_active=True
        ).first()
        if mapping:
            employee_id = mapping.employee_id
        else:
            emp = Employee.query.filter_by(employee_code=ext_code).first()
            if emp:
                employee_id = emp.id
        emp_cache[ext_code] = employee_id
        return employee_id

    # ----------------------------------------------------------------------
    # MODE 1: separate PunchIn / PunchOut per row (like your current API)
    # ----------------------------------------------------------------------
    if mode == "separate":
        for row in rows:
            if not isinstance(row, dict):
                continue

            ext_code = str(row.get(map_emp_code) or "").strip()
            if not ext_code:
                continue

            employee_id = resolve_employee_id(ext_code)
            if not employee_id:
                unmapped_count += 1
                unmapped_codes.add(ext_code)
                continue  # ✅ DO NOT store punches with NULL employee_id

            device_code = str(row.get(map_device) or "").strip() or None

            in_raw = row.get(map_in_time)
            out_raw = row.get(map_out_time)

            # IN
            if in_raw:
                try:
                    dt_in = datetime.strptime(str(in_raw), time_fmt)
                    db.session.add(BiometricPunch(
                        sync_run_id=run.id,
                        employee_id=employee_id,
                        external_employee_code=ext_code,
                        punch_time=dt_in,
                        direction="in",
                        device_code=device_code,
                        source_type="api",
                        source_name=run.source_name,
                        raw_data=json.dumps(row),
                    ))
                    total_saved += 1
                except Exception:
                    current_app.logger.exception("Failed to parse IN time %s", in_raw)

            # OUT
            if out_raw and out_raw != in_raw:
                try:
                    dt_out = datetime.strptime(str(out_raw), time_fmt)
                    db.session.add(BiometricPunch(
                        sync_run_id=run.id,
                        employee_id=employee_id,
                        external_employee_code=ext_code,
                        punch_time=dt_out,
                        direction="out",
                        device_code=device_code,
                        source_type="api",
                        source_name=run.source_name,
                        raw_data=json.dumps(row),
                    ))
                    total_saved += 1
                except Exception:
                    current_app.logger.exception("Failed to parse OUT time %s", out_raw)

        if unmapped_count:
            examples = ", ".join(list(unmapped_codes)[:10])
            run.message = f"Skipped unmapped biometric codes: {unmapped_count} (e.g. {examples})"

        db.session.commit()
        return total_saved

    # ----------------------------------------------------------------------
    # MODE 2: single punch field, possibly multiple rows per employee
    #         → we infer direction: IN / OUT alternating by time
    # ----------------------------------------------------------------------
    punches_by_emp: dict[str, list[dict]] = {}

    for row in rows:
        if not isinstance(row, dict):
            continue

        ext_code = str(row.get(map_emp_code) or "").strip()
        if not ext_code:
            continue

        t_raw = row.get(map_punch_time)
        if not t_raw:
            continue

        try:
            dt = datetime.strptime(str(t_raw), time_fmt)
        except Exception:
            current_app.logger.exception("Failed to parse punch time %s", t_raw)
            continue

        raw_dir_val = row.get(map_direction) if map_direction else None
        device_code = str(row.get(map_device) or "").strip() or None

        punches_by_emp.setdefault(ext_code, []).append({
            "dt": dt,
            "raw_dir": raw_dir_val,
            "device_code": device_code,
            "row": row,
        })

    for ext_code, items in punches_by_emp.items():
        employee_id = resolve_employee_id(ext_code)
        if not employee_id:
            unmapped_count += 1
            unmapped_codes.add(ext_code)
            continue  # ✅ DO NOT store punches with NULL employee_id

        # sort by time
        items.sort(key=lambda x: x["dt"])

        for idx, item in enumerate(items):
            raw_dir = item["raw_dir"]
            direction = None

            # If API actually gives a direction column, normalise it
            if raw_dir is not None:
                s = str(raw_dir).strip().lower()
                if s in ("in", "i", "1", "entry", "enter"):
                    direction = "in"
                elif s in ("out", "o", "0", "exit", "leave"):
                    direction = "out"

            # If still unknown: alternate by index
            # idx 0 -> IN, idx 1 -> OUT, idx 2 -> IN ...
            if not direction:
                direction = "in" if idx % 2 == 0 else "out"

            db.session.add(BiometricPunch(
                sync_run_id=run.id,
                employee_id=employee_id,
                external_employee_code=ext_code,
                punch_time=item["dt"],
                direction=direction,
                device_code=item["device_code"],
                source_type="api",
                source_name=run.source_name,
                raw_data=json.dumps(item["row"]),
            ))
            total_saved += 1

    if unmapped_count:
        examples = ", ".join(list(unmapped_codes)[:10])
        run.message = f"Skipped unmapped biometric codes: {unmapped_count} (e.g. {examples})"

    db.session.commit()
    return total_saved


# ---------------------------------------------------------------------------
# DB-based sync stub (future)
# ---------------------------------------------------------------------------

def _sync_biometric_from_db(cfg: dict, target_date: date, run: BiometricSyncRun) -> int:
    """
    Placeholder for DB-based sync. When you plug in an actual biometric DB,
    SELECT rows for target_date, map to BiometricPunch similarly to API case.
    """
    current_app.logger.warning("DB-based biometric sync not implemented yet.")
    return 0


def get_biometric_day_summary(employee_id: int, target_date: date, source_name: str | None = None):
    """
    Returns a dict with first IN, last OUT and total hours for a given employee/date
    based on BiometricPunch.

    IMPORTANT FIX:
      - source_name default is None, so we do NOT hide punches that are not "default".
    """
    start_dt = datetime.combine(target_date, time.min)
    end_dt = datetime.combine(target_date + timedelta(days=1), time.min)

    base_q = BiometricPunch.query.filter(
        BiometricPunch.employee_id == employee_id,
        BiometricPunch.punch_time >= start_dt,
        BiometricPunch.punch_time < end_dt,
    )

    # filter by source if needed
    if source_name:
        base_q = base_q.filter(BiometricPunch.source_name == source_name)

    first_in = (base_q.filter(BiometricPunch.direction == "in")
                      .order_by(BiometricPunch.punch_time.asc())
                      .first())
    last_out = (base_q.filter(BiometricPunch.direction == "out")
                      .order_by(BiometricPunch.punch_time.desc())
                      .first())

    check_in = first_in.punch_time if first_in else None
    check_out = last_out.punch_time if last_out else None
    total_hours = None

    if check_in and check_out and check_out > check_in:
        total_hours = (check_out - check_in).total_seconds() / 3600.0

    return {
        "check_in": check_in,
        "check_out": check_out,
        "total_hours": total_hours,
        "has_data": bool(first_in or last_out),
    }