"""Helpers for per file_path upload schedules (30 min cycle: active + rest)."""
from __future__ import annotations

import os
from datetime import datetime, timedelta
from typing import Any, Dict, Iterable, List, Optional, Tuple

from app.core.config import settings
from app.services.document_db_service import DocumentDbService
from app.services.file_schedule_service import FileScheduleService


def clear_upload_scheduling_for_paths(
    *,
    agent_id: Optional[str],
    paths: Iterable[str],
    schedule_svc: Optional[FileScheduleService] = None,
    scan_metadata_db_file: Optional[str] = None,
) -> Dict[str, int]:
    """
    After file/folder delete: drop file_schedules rows, folder_scan_jobs, and
    cancel any in-memory scheduler so the path can be uploaded again.
    """
    from app.utils.folder_scan_scheduler import cancel_scheduled_folder_scans_for_paths

    path_list: List[str] = [
        os.path.normpath(os.path.abspath(str(p).strip()))
        for p in paths
        if p and str(p).strip()
    ]
    if not path_list:
        return {"schedules_deleted": 0, "jobs_deleted": 0, "schedulers_cancelled": 0}

    svc = schedule_svc or FileScheduleService()
    aid = str(agent_id or "").strip() or None
    schedulers_cancelled = 0
    if aid:
        schedulers_cancelled = cancel_scheduled_folder_scans_for_paths(path_list, aid)

    schedules_deleted = svc.delete_file_schedules_touching_paths(
        paths=path_list,
        agent_id=aid,
    )

    jobs_deleted = 0
    if scan_metadata_db_file:
        jobs_deleted = DocumentDbService(
            db_file=scan_metadata_db_file
        ).delete_folder_scan_jobs_touching_paths(path_list)

    return {
        "schedules_deleted": schedules_deleted,
        "jobs_deleted": jobs_deleted,
        "schedulers_cancelled": schedulers_cancelled,
    }


def folder_scan_schedule_seconds() -> Tuple[int, int, int]:
    """Return (cycle_seconds, active_seconds, rest_seconds) from settings."""
    cycle_min = max(1, int(getattr(settings, "FOLDER_SCAN_SCHEDULE_CYCLE_MINUTES", 30) or 30))
    active_min = max(1, int(getattr(settings, "FOLDER_SCAN_SCHEDULE_ACTIVE_MINUTES", 20) or 20))
    rest_min = max(1, int(getattr(settings, "FOLDER_SCAN_SCHEDULE_REST_MINUTES", 10) or 10))
    if active_min + rest_min != cycle_min:
        rest_min = max(1, cycle_min - active_min)
    return cycle_min * 60, active_min * 60, rest_min * 60


def seconds_until_phase_end(schedule_row: Optional[Dict[str, Any]] = None) -> int:
    """Seconds remaining in the current phase (active or rest) of the global scheduler."""
    del schedule_row  # global heartbeat only
    status = FileScheduleService().get_scheduler_status()
    if not status:
        return 0
    next_at = status.get("next_phase_at")
    if not next_at:
        return 0
    try:
        end = datetime.fromisoformat(str(next_at))
    except ValueError:
        return 0
    return max(0, int((end - datetime.now()).total_seconds()))


def remaining_rest_after_upload_overflow(
    scheduler_status: Optional[Dict[str, Any]] = None,
    *,
    as_of: Optional[datetime] = None,
) -> Tuple[int, datetime, datetime]:
    """
    Full rest period starting when the last upload finishes (``as_of``).

    Upload work that runs into the scheduled rest window does not shorten rest:
    e.g. 10 min rest with 2 min of upload during that window still yields 10 min
    rest from ``as_of``; the server becomes active again at ``as_of`` + rest_seconds.

    Returns (remaining_seconds, rest_window_started_at, next_phase_at).
    """
    status = scheduler_status or FileScheduleService().get_scheduler_status()
    now = as_of or datetime.now()
    rest_s = max(1, int((status or {}).get("rest_seconds") or 600))
    return rest_s, now, now + timedelta(seconds=rest_s)


def is_global_scheduler_resting() -> bool:
    """True when the global upload scheduler is in its rest window."""
    return global_scheduler_rest_phase() is not None


def global_scheduler_rest_phase() -> Optional[Dict[str, Any]]:
    """
    When the global upload scheduler is in its rest window, return timing details.

    Returns None during the active processing window (or when scheduler status is unset).
    """
    status = FileScheduleService().get_scheduler_status()
    if not status:
        return None
    phase = (status.get("phase") or "active").lower()
    if phase != "rest":
        return None
    remaining_secs = seconds_until_phase_end()
    return {
        "phase": "rest",
        "seconds_until_phase_end": remaining_secs,
        "remaining_label": format_phase_remaining(remaining_secs),
        "next_phase_at": status.get("next_phase_at"),
    }


def advance_file_schedule_if_due(
    schedule_svc: FileScheduleService,
    *,
    file_path: str,
    agent_id: Optional[str],
) -> Optional[Dict[str, Any]]:
    """No-op for per-path advancing; returns current schedule row."""
    return schedule_svc.get_file_schedule(file_path=file_path, agent_id=agent_id)


def active_file_path_schedule(
    file_path: str,
    agent_id: str,
    *,
    schedule_svc: Optional[FileScheduleService] = None,
) -> Optional[Dict[str, Any]]:
    """
    Return an active (pending/processing) schedule for this path+agent, if any.

    Terminal DB rows (completed/failed/cancelled) and in-memory ``cancelling`` do
    not block re-upload after stop or delete.
    """
    path_norm = os.path.normpath(os.path.abspath(str(file_path or "").strip()))
    aid = str(agent_id or "").strip()
    if not path_norm or not aid:
        return None

    svc = schedule_svc or FileScheduleService()
    row = svc.get_file_schedule(file_path=path_norm, agent_id=aid)
    if row:
        return row

    latest = svc.get_file_schedule_row(file_path=path_norm, agent_id=aid)
    if latest:
        latest_status = str(latest.get("status") or "").lower()
        if latest_status in ("completed", "failed", "cancelled"):
            return None

    from app.utils.folder_scan_scheduler import (
        _UPLOAD_BLOCKING_SCHEDULER_STATUSES,
        find_scheduled_job_for_file_path,
    )

    mem = find_scheduled_job_for_file_path(path_norm, aid)
    if mem and (mem.get("status") or "").lower() in _UPLOAD_BLOCKING_SCHEDULER_STATUSES:
        return {
            "file_path": path_norm,
            "agent_id": aid,
            "job_id": mem.get("job_id"),
            "root_path": mem.get("file_path") or path_norm,
            "status": mem.get("status"),
            "schedule_enabled": 1,
        }
    return None


def dedupe_schedule_list_by_file_path(
    schedules: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
    """Keep one list row per ``file_path`` (highest ``id`` wins)."""
    by_path: Dict[str, Dict[str, Any]] = {}
    for row in schedules:
        fp = str(row.get("file_path") or "")
        if not fp:
            continue
        prev = by_path.get(fp)
        if prev is None or int(row.get("id") or 0) >= int(prev.get("id") or 0):
            by_path[fp] = row
    return list(by_path.values())


def ensure_file_schedule_row(
    schedule_svc: FileScheduleService,
    *,
    file_path: str,
    agent_id: str,
    schedule_enabled: bool = True,
    cycle_seconds: Optional[int] = None,
    active_seconds: Optional[int] = None,
    rest_seconds: Optional[int] = None,
    job_id: Optional[str] = None,
) -> Optional[str]:
    """Queue folder/file in file_schedules; returns job_id to use."""
    return schedule_svc.upsert_file_schedule(
        file_path=file_path,
        agent_id=agent_id,
        status="pending" if schedule_enabled else "completed",
        job_id=job_id,
        root_path=file_path,
    )


def format_phase_remaining(seconds: int) -> str:
    """Human-readable countdown for active/rest windows (e.g. 20 minutes → 45 seconds)."""
    s = max(0, int(seconds))
    if s >= 3600:
        hours, rem = divmod(s, 3600)
        minutes = rem // 60
        parts = [f"{hours} hour{'s' if hours != 1 else ''}"]
        if minutes:
            parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
        return " ".join(parts)
    if s >= 60:
        minutes, secs = divmod(s, 60)
        min_part = f"{minutes} minute{'s' if minutes != 1 else ''}"
        if secs:
            return f"{min_part} and {secs} second{'s' if secs != 1 else ''}"
        return min_part
    return f"{s} second{'s' if s != 1 else ''}"


def scheduler_status_message(
    *,
    phase: str,
    rest_minutes: int,
    active_minutes: int,
    seconds_remaining: Optional[int] = None,
) -> str:
    """User-facing scheduler phase message (active vs rest/sleep)."""
    from app.services.folder_scan_job_prompts import (
        FOLDER_SCAN_SCHEDULER_ACTIVE,
        FOLDER_SCAN_SCHEDULER_ACTIVE_REMAINING,
        FOLDER_SCAN_SCHEDULER_SLEEP,
        FOLDER_SCAN_SCHEDULER_SLEEP_REMAINING,
    )

    phase_norm = (phase or "active").lower()
    if seconds_remaining is not None:
        remaining_label = format_phase_remaining(seconds_remaining)
        if phase_norm == "rest":
            return FOLDER_SCAN_SCHEDULER_SLEEP_REMAINING.format(remaining=remaining_label)
        return FOLDER_SCAN_SCHEDULER_ACTIVE_REMAINING.format(remaining=remaining_label)
    if phase_norm == "rest":
        return FOLDER_SCAN_SCHEDULER_SLEEP.format(minutes=max(1, int(rest_minutes)))
    return FOLDER_SCAN_SCHEDULER_ACTIVE


def build_folder_scan_step_user_message(
    *,
    job_completed: bool,
    uploaded: int = 0,
    skipped: int = 0,
    schedule_row: Optional[Dict[str, Any]] = None,
    phase: str = "active",
    resting: bool = False,
) -> tuple[str, str]:
    """
    Build (user_message, scheduler_message) for /scan/step responses.

    When the job finished, states that all files are uploaded; if the active window
    is still open, appends scheduler countdown with minutes and seconds.
    """
    from app.services.folder_scan_job_prompts import (
        FOLDER_SCAN_ALL_FILES_UPLOADED,
        FOLDER_SCAN_ALL_FILES_UPLOADED_WITH_SCHEDULE,
        FOLDER_SCAN_UPLOAD_IN_PROGRESS,
    )

    sched_msg = scheduler_message_from_schedule_row(schedule_row)
    phase_norm = (phase or (schedule_row or {}).get("phase") or "active").lower()
    uploaded_n = int(uploaded or 0)
    skipped_n = int(skipped or 0)

    if job_completed:
        remaining_secs = (
            seconds_until_phase_end(schedule_row) if schedule_row else 0
        )
        if phase_norm == "active" and remaining_secs > 0:
            remaining = format_phase_remaining(remaining_secs)
            user_msg = FOLDER_SCAN_ALL_FILES_UPLOADED_WITH_SCHEDULE.format(
                uploaded=uploaded_n,
                skipped=skipped_n,
                remaining=remaining,
            )
        else:
            user_msg = FOLDER_SCAN_ALL_FILES_UPLOADED.format(
                uploaded=uploaded_n,
                skipped=skipped_n,
            )
        return user_msg, sched_msg

    if resting or phase_norm == "rest":
        user_msg = (
            f"{sched_msg} The background scheduler will resume automatically "
            "when the rest window ends."
        )
    else:
        user_msg = FOLDER_SCAN_UPLOAD_IN_PROGRESS.format(
            uploaded=uploaded_n,
            skipped=skipped_n,
            scheduler=sched_msg,
        )
    return user_msg, sched_msg


def scheduler_message_from_schedule_row(row: Optional[Dict[str, Any]]) -> str:
    """Build scheduler_message from the global scheduler_status (includes live countdown)."""
    status = FileScheduleService().get_scheduler_status()
    if not status:
        cycle_s, active_s, rest_s = folder_scan_schedule_seconds()
        return scheduler_status_message(
            phase="active",
            rest_minutes=max(1, rest_s // 60),
            active_minutes=max(1, active_s // 60),
            seconds_remaining=active_s,
        )
    phase = (status.get("phase") or "active").lower()
    return scheduler_status_message(
        phase=phase,
        rest_minutes=max(1, int(status.get("rest_seconds", 600) // 60)),
        active_minutes=max(1, int(status.get("active_seconds", 1200) // 60)),
        seconds_remaining=seconds_until_phase_end(row),
    )


def scan_step_poll_allowed(
    *,
    background: bool = False,
    completed: bool = False,
    scheduler_running: bool = False,
) -> bool:
    """
    Whether the UI should call /scan/step again.

    When a background scheduler owns the job, only a terminal status should
    trigger another step call (final summary). Manual batch mode steps until
    completed=false becomes false.
    """
    if background or scheduler_running:
        return bool(completed)
    return not completed


def resolve_schedule_status(
    schedule_row: Dict[str, Any],
    *,
    agent_id: str,
) -> str:
    """
    Human-readable schedule status for list APIs.

    Values: disabled | active | rest | running | cancelling | completed | failed | cancelled
    """
    if not int(schedule_row.get("schedule_enabled") or 0):
        db_status = str(schedule_row.get("status") or "").lower()
        if db_status in ("completed", "failed", "cancelled"):
            return db_status
        return "disabled"
    file_path = str(schedule_row.get("file_path") or "")
    aid = str(agent_id or "").strip()
    if file_path and aid:
        from app.utils.folder_scan_scheduler import find_scheduled_job_for_file_path

        bg = find_scheduled_job_for_file_path(file_path, aid)
        if bg:
            st = (bg.get("status") or "").lower()
            if st in ("starting", "running"):
                return "running"
            if st == "resting":
                return "rest"
            if st == "cancelling":
                return "cancelling"
            if st in ("completed", "failed", "cancelled"):
                return st
    status = FileScheduleService().get_scheduler_status()
    return (status.get("phase") if status else "active").lower()


def get_folder_scan_job_status(job_id: str) -> Optional[str]:
    """Return ``folder_scan_jobs.status`` for ``job_id``, or None if missing."""
    from app.services.document_service import FILE_SYS_METADATA_DB

    jid = str(job_id or "").strip()
    if not jid:
        return None
    try:
        svc = DocumentDbService(db_file=FILE_SYS_METADATA_DB)
        try:
            row = svc.get_folder_scan_job(jid)
        finally:
            svc.close()
        if not row:
            return None
        return str(row.get("status") or "").lower() or None
    except Exception:
        return None


def effective_schedule_status(
    schedule_row: Dict[str, Any],
    *,
    agent_id: str,
) -> str:
    """Unified schedule status: queue row terminal state wins, then folder_scan_jobs."""
    resolved = resolve_schedule_status(schedule_row, agent_id=agent_id)
    fs_status = str(schedule_row.get("status") or "").lower()
    if fs_status in ("completed", "failed", "cancelled"):
        return fs_status
    job_id = str(schedule_row.get("job_id") or "").strip()
    if not job_id:
        return resolved
    db_status = get_folder_scan_job_status(job_id)
    return db_status or resolved


def schedule_details_payload(row: Optional[Dict[str, Any]]) -> Dict[str, Any]:
    """API-friendly schedule snapshot from global scheduler_status."""
    status = FileScheduleService().get_scheduler_status()
    if not status:
        cycle_s, active_s, rest_s = folder_scan_schedule_seconds()
        return {
            "schedule_enabled": True,
            "phase": "active",
            "cycle_seconds": cycle_s,
            "active_seconds": active_s,
            "rest_seconds": rest_s,
            "seconds_until_phase_end": active_s,
        }
    phase = (status.get("phase") or "active").lower()
    enabled = True
    if row:
        enabled = bool(int(row.get("schedule_enabled") or 0))
    return {
        "schedule_enabled": enabled,
        "phase": phase,
        "cycle_seconds": status.get("active_seconds", 1200) + status.get("rest_seconds", 600),
        "active_seconds": status.get("active_seconds", 1200),
        "rest_seconds": status.get("rest_seconds", 600),
        "phase_started_at": status.get("phase_started_at"),
        "next_phase_at": status.get("next_phase_at"),
        "seconds_until_phase_end": seconds_until_phase_end(row),
    }


def scan_step_rest_response(
    *,
    job_id: str,
    file_path: str,
    schedule_row: Dict[str, Any],
) -> Dict[str, Any]:
    """Return when /scan/step is called during a rest window (client should retry later)."""
    retry = seconds_until_phase_end(schedule_row)
    return {
        "success": True,
        "message": scheduler_message_from_schedule_row(schedule_row)
        + ". The background scheduler will resume when the rest window ends.",
        "job_id": job_id,
        "file_path": file_path,
        "completed": False,
        "resting": True,
        "phase": "rest",
        "retry_after_seconds": retry,
        "uploaded_batch": 0,
        "skipped_batch": 0,
        "chunks_batch": 0,
        "schedule": schedule_details_payload(schedule_row),
        "scan_step_available": False,
        "background": True,
    }


def maybe_end_active_window(
    schedule_svc: FileScheduleService,
    *,
    file_path: str,
    agent_id: Optional[str],
) -> Optional[Dict[str, Any]]:
    """No-op for per-path active window management; returns current schedule row."""
    return schedule_svc.get_file_schedule(file_path=file_path, agent_id=agent_id)
