import json
from datetime import datetime, timedelta

from flask import Blueprint, current_app, flash, jsonify, redirect, render_template, request, session, url_for
from flask_babel import gettext as _
from flask_login import current_user, login_required
from sqlalchemy import inspect, text
from sqlalchemy.exc import ProgrammingError

from app import db, log_event, socketio, track_event
from app.constants import TimeEntrySource
from app.models import Activity, Client, Project, Settings, Task, TimeEntry, User
from app.services.client_service import ClientService
from app.services.project_service import ProjectService
from app.services.time_tracking_service import TimeTrackingService
from app.utils.db import safe_commit
from app.utils.error_handling import safe_log
from app.utils.posthog_funnels import track_onboarding_first_time_entry, track_onboarding_first_timer
from app.utils.scope_filter import user_can_access_client, user_can_access_project
from app.utils.timezone import parse_local_datetime, parse_user_local_datetime, utc_to_local

_project_service = ProjectService()
_client_service = ClientService()

timer_bp = Blueprint("timer", __name__)


def _parse_optional_int(value):
    """Return int(value) if value is a non-empty string that converts to int, else None."""
    if value is None or (isinstance(value, str) and not value.strip()):
        return None
    try:
        return int(value)
    except (ValueError, TypeError):
        return None


def _edit_timer_form_projects_tasks(timer, can_edit_schedule):
    """Active projects/tasks for the edit form; scoped for subcontractors."""
    from app.utils.scope_filter import apply_project_scope_to_model

    projects = []
    tasks = []
    projects_query = Project.query.filter_by(status="active").order_by(Project.name)
    scope_p = apply_project_scope_to_model(Project, current_user)
    if scope_p is not None:
        projects_query = projects_query.filter(scope_p)
    if current_user.is_admin or scope_p is not None or can_edit_schedule:
        projects = projects_query.all()
        if timer.project_id:
            tasks = Task.query.filter_by(project_id=timer.project_id).order_by(Task.name).all()
    return projects, tasks


def _edit_timer_render_kwargs(timer, can_edit_schedule, show_source_dropdown):
    projects, tasks = _edit_timer_form_projects_tasks(timer, can_edit_schedule)
    return {
        "timer": timer,
        "projects": projects,
        "tasks": tasks,
        "can_edit_schedule": can_edit_schedule,
        "show_source_dropdown": show_source_dropdown,
    }


@timer_bp.route("/timer/start", methods=["POST"])
@login_required
def start_timer():
    """Start a new timer for the current user"""
    from app.utils.client_lock import enforce_locked_client_id, get_locked_client_id

    project_id = _parse_optional_int(request.form.get("project_id"))
    client_id = _parse_optional_int(request.form.get("client_id"))
    client_id = enforce_locked_client_id(client_id)
    task_id = _parse_optional_int(request.form.get("task_id"))
    notes = request.form.get("notes", "").strip()
    template_id = _parse_optional_int(request.form.get("template_id"))
    current_app.logger.info(
        "POST /timer/start user=%s project_id=%s task_id=%s template_id=%s",
        current_user.username,
        project_id,
        task_id,
        template_id,
    )

    # Load template data if template_id is provided
    if template_id:
        from app.models import TimeEntryTemplate

        template = TimeEntryTemplate.query.filter_by(id=template_id, user_id=current_user.id).first()
        if template:
            # Override with template values if not explicitly set
            if not project_id and template.project_id:
                project_id = template.project_id
            if not task_id and template.task_id:
                task_id = template.task_id
            if not notes and template.default_notes:
                notes = template.default_notes
            # Mark template as used
            template.record_usage()
            db.session.commit()

    # Require either project or client
    if not project_id and not client_id:
        flash(_("Either a project or a client is required"), "error")
        current_app.logger.warning("Start timer failed: missing project_id and client_id")
        return redirect(url_for("main.dashboard"))

    project = None
    client = None

    # Validate project if provided
    if project_id:
        project = _project_service.get_by_id(project_id)
        if not project:
            flash(_("Invalid project selected"), "error")
            current_app.logger.warning("Start timer failed: invalid project_id=%s", project_id)
            return redirect(url_for("main.dashboard"))

        locked_id = get_locked_client_id()
        if locked_id and getattr(project, "client_id", None) and int(project.client_id) != int(locked_id):
            flash(_("Selected project does not match the locked client."), "error")
            return redirect(url_for("main.dashboard"))

        # Check if project is active (not archived or inactive)
        if project.status == "archived":
            flash(_("Cannot start timer for an archived project. Please unarchive the project first."), "error")
            current_app.logger.warning("Start timer failed: project_id=%s is archived", project_id)
            return redirect(url_for("main.dashboard"))
        elif project.status != "active":
            flash(_("Cannot start timer for an inactive project"), "error")
            current_app.logger.warning("Start timer failed: project_id=%s is not active", project_id)
            return redirect(url_for("main.dashboard"))

        # If a task is provided, validate it belongs to the project
        if task_id:
            task = Task.query.filter_by(id=task_id, project_id=project_id).first()
            if not task:
                flash(_("Selected task is invalid for the chosen project"), "error")
                current_app.logger.warning(
                    "Start timer failed: task_id=%s does not belong to project_id=%s", task_id, project_id
                )
                return redirect(url_for("main.dashboard"))
        else:
            task = None
    else:
        task = None

    # Validate client if provided (and no project)
    if client_id and not project_id:
        client = _client_service.get_by_id(client_id)
        if not client or client.status != "active":
            flash(_("Invalid client selected"), "error")
            current_app.logger.warning("Start timer failed: invalid client_id=%s", client_id)
            return redirect(url_for("main.dashboard"))

        # Tasks are not allowed for client-only timers
        if task_id:
            flash(_("Tasks can only be selected for project-based timers"), "error")
            current_app.logger.warning(
                "Start timer failed: task_id=%s provided for client-only timer (client_id=%s)", task_id, client_id
            )
            return redirect(url_for("main.dashboard"))

    # Subcontractor scope: only allow starting timer on assigned project/client
    if project_id and not user_can_access_project(current_user, project_id):
        flash(_("You do not have access to this project"), "error")
        current_app.logger.warning("Start timer denied: user has no access to project_id=%s", project_id)
        return redirect(url_for("main.dashboard"))
    if client_id and not project_id and not user_can_access_client(current_user, client_id):
        flash(_("You do not have access to this client"), "error")
        current_app.logger.warning("Start timer denied: user has no access to client_id=%s", client_id)
        return redirect(url_for("main.dashboard"))

    can_start, _unused = TimeTrackingService().can_start_timer(current_user.id)
    if not can_start:
        flash(_("You already have an active timer. Stop it before starting a new one."), "error")
        current_app.logger.info("Start timer blocked: user already has an active timer")
        return redirect(url_for("main.dashboard"))

    # Validate time entry requirements (task, description)
    from app.utils.time_entry_validation import validate_time_entry_requirements

    settings = Settings.get_settings()
    err = validate_time_entry_requirements(
        settings,
        project_id=project_id,
        client_id=client_id if client_id and not project_id else None,
        task_id=task.id if task else task_id,
        notes=notes if notes else None,
    )
    if err:
        flash(_(err["message"]), "error")
        return redirect(url_for("main.dashboard"))

    # Create new timer
    from app.models.time_entry import local_now

    new_timer = TimeEntry(
        user_id=current_user.id,
        project_id=project_id if project_id else None,
        client_id=client_id if client_id and not project_id else None,
        task_id=task.id if task else None,
        start_time=local_now(),
        notes=notes if notes else None,
        source="auto",
    )

    db.session.add(new_timer)
    if not safe_commit(
        "start_timer",
        {
            "user_id": current_user.id,
            "project_id": project_id,
            "client_id": client_id,
            "task_id": task_id,
        },
    ):
        flash(_("Could not start timer due to a database error. Please check server logs."), "error")
        return redirect(url_for("main.dashboard"))
    current_app.logger.info(
        "Started new timer id=%s for user=%s project_id=%s client_id=%s task_id=%s",
        new_timer.id,
        current_user.username,
        project_id,
        client_id,
        task_id,
    )

    from app.telemetry.otel_setup import business_span

    with business_span(
        "timer.start",
        user_id=current_user.id,
        project_based=bool(project_id),
        client_only=bool(client_id and not project_id),
        has_task=bool(task_id),
    ):
        pass

    # Track timer started event
    log_event(
        "timer.started",
        user_id=current_user.id,
        project_id=project_id,
        client_id=client_id,
        task_id=task_id,
        description=notes,
    )
    track_event(
        current_user.id,
        "timer.started",
        {
            "project_id": project_id,
            "client_id": client_id,
            "task_id": task_id,
            "has_description": bool(notes),
        },
    )

    # Log activity
    Activity.log(
        user_id=current_user.id,
        action="started",
        entity_type="time_entry",
        entity_id=new_timer.id,
        entity_name=(f"{project.name}" if project else f"{client.name if client else _('Unknown')}")
        + (f" - {task.name}" if task else ""),
        description=(
            f"Started timer for {project.name}"
            if project
            else f"Started timer for {client.name if client else _('Unknown')}"
        )
        + (f" - {task.name}" if task else ""),
        extra_data={"project_id": project_id, "client_id": client_id, "task_id": task_id},
        ip_address=request.remote_addr,
        user_agent=request.headers.get("User-Agent"),
    )

    # Check if this is user's first timer (onboarding milestone)
    timer_count = TimeEntry.query.filter_by(user_id=current_user.id, source="auto").count()

    if timer_count == 1:  # First timer ever
        track_onboarding_first_timer(
            current_user.id,
            {
                "project_id": project_id,
                "client_id": client_id,
                "has_task": bool(task_id),
                "has_notes": bool(notes),
            },
        )

    # Emit WebSocket event for real-time updates
    try:
        payload = {
            "user_id": current_user.id,
            "timer_id": new_timer.id,
            "project_name": project.name if project else None,
            "client_name": client.name if client else None,
            "start_time": new_timer.start_time.isoformat(),
        }
        if task:
            payload["task_id"] = task.id
            payload["task_name"] = task.name
        socketio.emit("timer_started", payload)
    except Exception as e:
        current_app.logger.warning("Socket emit failed for timer_started: %s", e)

    # Invalidate dashboard cache so timer appears immediately
    try:
        from app.utils.cache import invalidate_dashboard_for_user

        invalidate_dashboard_for_user(current_user.id)
        current_app.logger.debug("Invalidated dashboard cache for user %s", current_user.id)
    except Exception as e:
        current_app.logger.warning("Failed to invalidate dashboard cache: %s", e)

    if task:
        flash(f"Timer started for {project.name} - {task.name}", "success")
    elif project:
        flash(f"Timer started for {project.name}", "success")
    elif client:
        flash(f"Timer started for {client.name}", "success")
    else:
        flash(_("Timer started"), "success")
    return redirect(url_for("main.dashboard"))


@timer_bp.route("/timer/start/from-template/<int:template_id>", methods=["GET", "POST"])
@login_required
def start_timer_from_template(template_id):
    """Start a timer directly from a template"""
    from app.models import TimeEntryTemplate

    # Load template
    template = TimeEntryTemplate.query.filter_by(id=template_id, user_id=current_user.id).first_or_404()

    can_start, _unused = TimeTrackingService().can_start_timer(current_user.id)
    if not can_start:
        flash(_("You already have an active timer. Stop it before starting a new one."), "error")
        return redirect(url_for("main.dashboard"))

    # Validate template has required data
    if not template.project_id:
        flash(_("Template must have a project to start a timer"), "error")
        return redirect(url_for("time_entry_templates.list_templates"))

    # Check if project is active
    project = _project_service.get_by_id(template.project_id)
    if not project or project.status != "active":
        flash(_("Cannot start timer for this project"), "error")
        return redirect(url_for("time_entry_templates.list_templates"))

    if not user_can_access_project(current_user, template.project_id):
        flash(_("You do not have access to this project"), "error")
        current_app.logger.warning(
            "Start timer from template denied: user has no access to project_id=%s", template.project_id
        )
        return redirect(url_for("time_entry_templates.list_templates"))

    # Create new timer from template
    from app.models.time_entry import local_now

    new_timer = TimeEntry(
        user_id=current_user.id,
        project_id=template.project_id,
        task_id=template.task_id,
        start_time=local_now(),
        notes=template.default_notes,
        tags=template.tags,
        source="auto",
        billable=template.billable,
    )

    db.session.add(new_timer)

    # Mark template as used
    template.record_usage()

    if not safe_commit("start_timer_from_template", {"template_id": template_id}):
        flash(_("Could not start timer due to a database error. Please check server logs."), "error")
        return redirect(url_for("time_entry_templates.list_templates"))

    from app.telemetry.otel_setup import business_span

    with business_span(
        "timer.start",
        user_id=current_user.id,
        source="template",
        template_id=template_id,
        project_id=template.project_id,
    ):
        pass

    # Track events
    log_event(
        "timer.started.from_template", user_id=current_user.id, template_id=template_id, project_id=template.project_id
    )
    track_event(
        current_user.id,
        "timer.started.from_template",
        {
            "template_id": template_id,
            "template_name": template.name,
            "project_id": template.project_id,
            "has_task": bool(template.task_id),
        },
    )

    # Invalidate dashboard cache so timer appears immediately
    try:
        from app.utils.cache import invalidate_dashboard_for_user

        invalidate_dashboard_for_user(current_user.id)
        current_app.logger.debug("Invalidated dashboard cache for user %s", current_user.id)
    except Exception as e:
        current_app.logger.warning("Failed to invalidate dashboard cache: %s", e)

    flash(f'Timer started from template "{template.name}"', "success")
    return redirect(url_for("main.dashboard"))


@timer_bp.route("/timer/start/<int:project_id>")
@login_required
def start_timer_for_project(project_id):
    """Start a timer for a specific project (GET route for direct links)"""
    task_id = request.args.get("task_id", type=int)
    current_app.logger.info("GET /timer/start/%s user=%s task_id=%s", project_id, current_user.username, task_id)

    # Check if project exists
    project = _project_service.get_by_id(project_id)
    if not project:
        flash(_("Invalid project selected"), "error")
        current_app.logger.warning("Start timer (GET) failed: invalid project_id=%s", project_id)
        return redirect(url_for("main.dashboard"))

    # Check if project is active (not archived or inactive)
    if project.status == "archived":
        flash(_("Cannot start timer for an archived project. Please unarchive the project first."), "error")
        current_app.logger.warning("Start timer (GET) failed: project_id=%s is archived", project_id)
        return redirect(url_for("main.dashboard"))
    elif project.status != "active":
        flash(_("Cannot start timer for an inactive project"), "error")
        current_app.logger.warning("Start timer (GET) failed: project_id=%s is not active", project_id)
        return redirect(url_for("main.dashboard"))

    if not user_can_access_project(current_user, project_id):
        flash(_("You do not have access to this project"), "error")
        current_app.logger.warning("Start timer (GET) denied: user has no access to project_id=%s", project_id)
        return redirect(url_for("main.dashboard"))

    can_start, _unused = TimeTrackingService().can_start_timer(current_user.id)
    if not can_start:
        flash(_("You already have an active timer. Stop it before starting a new one."), "error")
        current_app.logger.info("Start timer (GET) blocked: user already has an active timer")
        return redirect(url_for("main.dashboard"))

    # Create new timer
    from app.models.time_entry import local_now

    new_timer = TimeEntry(
        user_id=current_user.id, project_id=project_id, task_id=task_id, start_time=local_now(), source="auto"
    )

    db.session.add(new_timer)
    if not safe_commit(
        "start_timer_for_project", {"user_id": current_user.id, "project_id": project_id, "task_id": task_id}
    ):
        flash(_("Could not start timer due to a database error. Please check server logs."), "error")
        return redirect(url_for("main.dashboard"))
    current_app.logger.info(
        "Started new timer id=%s for user=%s project_id=%s task_id=%s",
        new_timer.id,
        current_user.username,
        project_id,
        task_id,
    )

    from app.telemetry.otel_setup import business_span

    with business_span(
        "timer.start",
        user_id=current_user.id,
        source="project_link",
        project_id=project_id,
        has_task=bool(task_id),
    ):
        pass

    # Emit WebSocket event for real-time updates
    try:
        socketio.emit(
            "timer_started",
            {
                "user_id": current_user.id,
                "timer_id": new_timer.id,
                "project_name": project.name,
                "task_id": task_id,
                "start_time": new_timer.start_time.isoformat(),
            },
        )
    except Exception as e:
        current_app.logger.warning("Socket emit failed for timer_started (GET): %s", e)

    # Invalidate dashboard cache so timer appears immediately
    try:
        from app.utils.cache import invalidate_dashboard_for_user

        invalidate_dashboard_for_user(current_user.id)
        current_app.logger.debug("Invalidated dashboard cache for user %s", current_user.id)
    except Exception as e:
        current_app.logger.warning("Failed to invalidate dashboard cache: %s", e)

    if task_id:
        task = Task.query.get(task_id)
        task_name = task.name if task else "Unknown Task"
        flash(f"Timer started for {project.name} - {task_name}", "success")
    else:
        flash(f"Timer started for {project.name}", "success")

    return redirect(url_for("main.dashboard"))


@timer_bp.route("/timer/stop", methods=["POST"])
@login_required
def stop_timer():
    """Stop the current user's active timer"""
    active_timer = current_user.active_timer
    current_app.logger.info("POST /timer/stop user=%s active_timer=%s", current_user.username, bool(active_timer))

    if not active_timer:
        flash(_("No active timer to stop"), "error")
        return redirect(url_for("main.dashboard"))

    # Stop the timer
    try:
        active_timer.stop_timer()
        current_app.logger.info("Stopped timer id=%s for user=%s", active_timer.id, current_user.username)

        from app.telemetry.otel_setup import business_span

        duration_seconds = active_timer.duration_seconds if active_timer.duration_seconds else 0
        with business_span(
            "timer.stop",
            user_id=current_user.id,
            duration_seconds=int(duration_seconds) if duration_seconds is not None else 0,
        ):
            pass

        # Track timer stopped event
        log_event(
            "timer.stopped",
            user_id=current_user.id,
            time_entry_id=active_timer.id,
            project_id=active_timer.project_id,
            task_id=active_timer.task_id,
            duration_seconds=duration_seconds,
        )
        track_event(
            current_user.id,
            "timer.stopped",
            {
                "time_entry_id": active_timer.id,
                "project_id": active_timer.project_id,
                "task_id": active_timer.task_id,
                "duration_seconds": duration_seconds,
            },
        )

        # Log activity
        project_name = active_timer.project.name if active_timer.project else "No project"
        task_name = active_timer.task.name if active_timer.task else None
        Activity.log(
            user_id=current_user.id,
            action="stopped",
            entity_type="time_entry",
            entity_id=active_timer.id,
            entity_name=f"{project_name}" + (f" - {task_name}" if task_name else ""),
            description=f"Stopped timer for {project_name}"
            + (f" - {task_name}" if task_name else "")
            + f" - Duration: {active_timer.duration_formatted}",
            extra_data={
                "duration_hours": active_timer.duration_hours,
                "project_id": active_timer.project_id,
                "task_id": active_timer.task_id,
            },
            ip_address=request.remote_addr,
            user_agent=request.headers.get("User-Agent"),
        )

        # Check if this is user's first completed time entry (onboarding milestone)
        entry_count = TimeEntry.query.filter_by(user_id=current_user.id).filter(TimeEntry.end_time.isnot(None)).count()

        if entry_count == 1:  # First completed time entry ever
            track_onboarding_first_time_entry(
                current_user.id,
                {"source": "timer", "duration_seconds": duration_seconds, "has_task": bool(active_timer.task_id)},
            )

        # Emit WebSocket event for real-time updates
        try:
            socketio.emit(
                "timer_stopped",
                {"user_id": current_user.id, "timer_id": active_timer.id, "duration": active_timer.duration_formatted},
            )
        except Exception as e:
            current_app.logger.warning("Socket emit failed for timer_stopped: %s", e)

        # Invalidate dashboard cache so timer disappears immediately
        try:
            from app.utils.cache import invalidate_dashboard_for_user

            invalidate_dashboard_for_user(current_user.id)
            current_app.logger.debug("Invalidated dashboard cache for user %s", current_user.id)
        except Exception as e:
            current_app.logger.warning("Failed to invalidate dashboard cache: %s", e)

        # Pass data for post-timer toast (message + link to time entries; no flash to avoid duplicate)
        project_name = (
            active_timer.project.name
            if active_timer.project
            else (active_timer.client.name if active_timer.client else _("No project"))
        )
        session["timer_stopped_toast"] = {
            "duration": active_timer.duration_formatted,
            "project_name": project_name,
        }
        session.modified = True
        return redirect(url_for("main.dashboard"))
    except ValueError as e:
        # Timer already stopped or invalid state
        current_app.logger.warning("Cannot stop timer: %s", e)
        flash(_("Cannot stop timer: %(error)s", error=str(e)), "error")
        return redirect(url_for("main.dashboard"))
    except Exception as e:
        current_app.logger.exception("Error stopping timer: %s", e)
        flash(
            _("Could not stop timer due to an error. Please try again or contact support if the problem persists."),
            "error",
        )
        return redirect(url_for("main.dashboard"))


@timer_bp.route("/timer/pause", methods=["POST"])
@login_required
def pause_timer():
    """Pause the current user's active timer (clock stops; break accumulates on resume)."""
    active_timer = current_user.active_timer
    if not active_timer:
        flash(_("No active timer to pause"), "error")
        return redirect(url_for("main.dashboard"))
    try:
        active_timer.pause_timer()
        flash(_("Timer paused"), "success")
    except ValueError as e:
        flash(_(str(e)), "error")
    except Exception as e:
        current_app.logger.exception("Error pausing timer: %s", e)
        flash(_("Could not pause timer. Please try again."), "error")
    try:
        from app.utils.cache import invalidate_dashboard_for_user

        invalidate_dashboard_for_user(current_user.id)
    except Exception as e:
        safe_log(current_app.logger, "debug", "Dashboard cache invalidation failed: %s", e)
    return redirect(url_for("main.dashboard"))


@timer_bp.route("/timer/resume", methods=["POST"])
@login_required
def resume_timer():
    """Resume a paused timer (time since pause is counted as break)."""
    active_timer = current_user.active_timer
    if not active_timer:
        flash(_("No active timer to resume"), "error")
        return redirect(url_for("main.dashboard"))
    try:
        active_timer.resume_timer()
        flash(_("Timer resumed"), "success")
    except ValueError as e:
        flash(_(str(e)), "error")
    except Exception as e:
        current_app.logger.exception("Error resuming timer: %s", e)
        flash(_("Could not resume timer. Please try again."), "error")
    try:
        from app.utils.cache import invalidate_dashboard_for_user

        invalidate_dashboard_for_user(current_user.id)
    except Exception as e:
        safe_log(current_app.logger, "debug", "Dashboard cache invalidation failed: %s", e)
    return redirect(url_for("main.dashboard"))


@timer_bp.route("/timer/adjust", methods=["POST"])
@login_required
def adjust_timer():
    """Adjust the active timer's start time by delta_minutes (positive = add time, negative = subtract)."""
    active_timer = current_user.active_timer
    if not active_timer:
        flash(_("No active timer to adjust"), "error")
        return redirect(url_for("main.dashboard"))

    try:
        delta_minutes = int(request.form.get("delta_minutes", 0))
    except (TypeError, ValueError):
        flash(_("Invalid adjustment value"), "error")
        return redirect(url_for("main.dashboard"))

    if delta_minutes == 0:
        return redirect(url_for("main.dashboard"))

    # Clamp to avoid extreme shifts (e.g. ±4 hours)
    delta_minutes = max(-240, min(240, delta_minutes))
    from app.models.time_entry import local_now

    new_start = active_timer.start_time - timedelta(minutes=delta_minutes)
    # Do not set start_time in the future
    now_local = local_now()
    if new_start > now_local:
        new_start = now_local
    active_timer.start_time = new_start
    active_timer.updated_at = now_local
    db.session.commit()

    try:
        from app.utils.cache import invalidate_dashboard_for_user

        invalidate_dashboard_for_user(current_user.id)
    except Exception as e:
        safe_log(current_app.logger, "debug", "Dashboard cache invalidation failed: %s", e)

    if request.headers.get("X-Requested-With") == "XMLHttpRequest":
        return jsonify({"success": True, "start_time": active_timer.start_time.isoformat()})
    return redirect(url_for("main.dashboard"))


@timer_bp.route("/timer/status")
@login_required
def timer_status():
    """Get current timer status as JSON"""
    active_timer = current_user.active_timer

    if not active_timer:
        return jsonify({"active": False, "timer": None})

    return jsonify(
        {
            "active": True,
            "timer": {
                "id": active_timer.id,
                "project_name": active_timer.project.name if active_timer.project else None,
                "client_name": active_timer.client.name if active_timer.client else None,
                "start_time": active_timer.start_time.isoformat(),
                "current_duration": active_timer.current_duration_seconds,
                "duration_formatted": active_timer.duration_formatted,
                "paused": getattr(active_timer, "is_paused", False),
                "paused_at": active_timer.paused_at.isoformat() if active_timer.paused_at else None,
                "break_seconds": getattr(active_timer, "break_seconds", None) or 0,
                "break_formatted": getattr(active_timer, "break_formatted", "00:00:00"),
            },
        }
    )


@timer_bp.route("/timer/edit/<int:timer_id>", methods=["GET", "POST"])
@login_required
def edit_timer(timer_id):
    """Edit a completed timer entry"""
    timer = TimeEntry.query.get_or_404(timer_id)

    can_edit_schedule = current_user.is_admin or (
        timer.user_id == current_user.id and current_user.has_permission("edit_own_time_entries")
    )
    show_source_dropdown = current_user.is_admin

    # Check if user can edit this timer
    if timer.user_id != current_user.id and not current_user.is_admin:
        flash(_("You can only edit your own timers"), "error")
        return redirect(url_for("main.dashboard"))

    if request.method == "POST":
        from app.utils.validation import sanitize_input

        # Get reason for change
        reason = sanitize_input(request.form.get("reason", "").strip(), max_length=500) or None

        # Use service layer for update to get enhanced audit logging
        from app.services import TimeTrackingService

        service = TimeTrackingService()

        # Prepare update parameters
        notes_raw = request.form.get("notes", "").strip()
        tags_raw = request.form.get("tags", "").strip()
        update_params = {
            "entry_id": timer_id,
            "user_id": current_user.id,
            "is_admin": current_user.is_admin,
            "notes": sanitize_input(notes_raw, max_length=2000) if notes_raw else None,
            "tags": sanitize_input(tags_raw, max_length=500) if tags_raw else None,
            "billable": request.form.get("billable") == "on",
            "paid": request.form.get("paid") == "on",
            "reason": reason,
        }

        # Update invoice number
        invoice_number = request.form.get("invoice_number", "").strip()
        update_params["invoice_number"] = invoice_number if invoice_number else None
        # Clear invoice number if marking as unpaid
        if update_params["paid"] is False:
            update_params["invoice_number"] = None

        # Admins and users with edit_own_time_entries can edit schedule, project, and task
        if can_edit_schedule:
            # Update project if changed
            new_project_id = request.form.get("project_id", type=int)
            if new_project_id and new_project_id != timer.project_id:
                new_project = Project.query.filter_by(id=new_project_id, status="active").first()
                if new_project:
                    update_params["project_id"] = new_project_id
                else:
                    flash(_("Invalid project selected"), "error")
                    return render_template(
                        "timer/edit_timer.html",
                        **_edit_timer_render_kwargs(timer, can_edit_schedule, show_source_dropdown),
                    )
            else:
                update_params["project_id"] = None  # Don't change if not provided

            # Update task if changed
            new_task_id = request.form.get("task_id", type=int)
            if new_task_id != timer.task_id:
                if new_task_id:
                    new_task = Task.query.filter_by(
                        id=new_task_id, project_id=update_params.get("project_id") or timer.project_id
                    ).first()
                    if new_task:
                        update_params["task_id"] = new_task_id
                    else:
                        flash(_("Invalid task selected for the chosen project"), "error")
                        return render_template(
                            "timer/edit_timer.html",
                            **_edit_timer_render_kwargs(timer, can_edit_schedule, show_source_dropdown),
                        )
                else:
                    update_params["task_id"] = None
            else:
                update_params["task_id"] = None  # Don't change if not provided

            # Update start and end times if provided
            start_date = request.form.get("start_date")
            start_time = request.form.get("start_time")
            end_date = request.form.get("end_date")
            end_time = request.form.get("end_time")
            break_time = (request.form.get("break_time") or "").strip()

            if start_date and start_time:
                try:
                    # Convert parsed UTC-aware to local naive to match model storage
                    parsed_start_utc = parse_local_datetime(start_date, start_time)
                    new_start_time = utc_to_local(parsed_start_utc).replace(tzinfo=None)

                    # Validate that start time is not in the future
                    from app.models.time_entry import local_now

                    current_time = local_now()
                    if new_start_time > current_time:
                        flash(_("Start time cannot be in the future"), "error")
                        return render_template(
                            "timer/edit_timer.html",
                            **_edit_timer_render_kwargs(timer, can_edit_schedule, show_source_dropdown),
                        )

                    update_params["start_time"] = new_start_time
                except ValueError:
                    flash(_("Invalid start date/time format"), "error")
                    return render_template(
                        "timer/edit_timer.html",
                        **_edit_timer_render_kwargs(timer, can_edit_schedule, show_source_dropdown),
                    )
            else:
                update_params["start_time"] = None

            if end_date and end_time:
                try:
                    # Convert parsed UTC-aware to local naive to match model storage
                    parsed_end_utc = parse_local_datetime(end_date, end_time)
                    new_end_time = utc_to_local(parsed_end_utc).replace(tzinfo=None)

                    # Validate that end time is after start time
                    start_time_for_validation = update_params.get("start_time") or timer.start_time
                    if new_end_time <= start_time_for_validation:
                        flash(_("End time must be after start time"), "error")
                        return render_template(
                            "timer/edit_timer.html",
                            **_edit_timer_render_kwargs(timer, can_edit_schedule, show_source_dropdown),
                        )

                    update_params["end_time"] = new_end_time
                except ValueError:
                    flash(_("Invalid end date/time format"), "error")
                    return render_template(
                        "timer/edit_timer.html",
                        **_edit_timer_render_kwargs(timer, can_edit_schedule, show_source_dropdown),
                    )
            else:
                update_params["end_time"] = None

            # Parse break time (HH:MM) to seconds; empty clears break
            import re

            if break_time:
                m = re.match(r"^(\d{1,3}):([0-5]\d)$", break_time.strip())
                update_params["break_seconds"] = (int(m.group(1)) * 3600 + int(m.group(2)) * 60) if m else 0
            else:
                update_params["break_seconds"] = 0

        # Call service layer to update
        result = service.update_entry(**update_params)

        if not result.get("success"):
            flash(_(result.get("message", "Could not update timer")), "error")
            return render_template(
                "timer/edit_timer.html",
                **_edit_timer_render_kwargs(timer, can_edit_schedule, show_source_dropdown),
            )

        entry = result.get("entry")

        # Log activity
        if entry:
            entity_name = entry.project.name if entry.project else (entry.client.name if entry.client else "Unknown")
            task_name = entry.task.name if entry.task else None

            Activity.log(
                user_id=current_user.id,
                action="updated",
                entity_type="time_entry",
                entity_id=entry.id,
                entity_name=f"{entity_name}" + (f" - {task_name}" if task_name else ""),
                description=f"Updated time entry for {entity_name}" + (f" - {task_name}" if task_name else ""),
                extra_data={
                    "project_name": entry.project.name if entry.project else None,
                    "client_name": entry.client.name if entry.client else None,
                    "task_name": task_name,
                },
                ip_address=request.remote_addr,
                user_agent=request.headers.get("User-Agent"),
            )

        # Invalidate dashboard cache for the timer owner so changes appear immediately
        try:
            from app.utils.cache import invalidate_dashboard_for_user

            invalidate_dashboard_for_user(timer.user_id)
            current_app.logger.debug("Invalidated dashboard cache for user %s after timer edit", timer.user_id)
        except Exception as e:
            current_app.logger.warning("Failed to invalidate dashboard cache: %s", e)

        flash(_("Timer updated successfully"), "success")
        return redirect(url_for("main.dashboard"))

    return render_template(
        "timer/edit_timer.html",
        **_edit_timer_render_kwargs(timer, can_edit_schedule, show_source_dropdown),
    )


@timer_bp.route("/timer/view/<int:timer_id>")
@login_required
def view_timer(timer_id):
    """View a time entry (read-only)"""
    timer = TimeEntry.query.get_or_404(timer_id)

    # Check if user can view this timer
    can_view_all = current_user.is_admin or current_user.has_permission("view_all_time_entries")
    if not can_view_all and timer.user_id != current_user.id:
        flash(_("You do not have permission to view this time entry"), "error")
        return redirect(url_for("main.dashboard"))

    # Get link templates for invoice_number (for clickable values)
    from sqlalchemy.exc import ProgrammingError

    from app.models import LinkTemplate

    link_templates_by_field = {}
    try:
        for template in LinkTemplate.get_active_templates():
            if template.field_key == "invoice_number":
                link_templates_by_field["invoice_number"] = template
    except ProgrammingError as e:
        # Handle case where link_templates table doesn't exist (migration not run)
        if "does not exist" in str(e.orig) or "relation" in str(e.orig).lower():
            current_app.logger.warning("link_templates table does not exist. Run migration: flask db upgrade")
            link_templates_by_field = {}
        else:
            raise

    # Time entry approvals: can current user request approval for this entry?
    from app.utils.module_helpers import is_module_enabled

    time_approvals_enabled = is_module_enabled("time_approvals")
    can_request_approval = False
    if time_approvals_enabled and timer.user_id == current_user.id and timer.end_time:
        try:
            from app.models.time_entry_approval import ApprovalStatus, TimeEntryApproval

            pending = TimeEntryApproval.query.filter_by(
                time_entry_id=timer.id,
                status=ApprovalStatus.PENDING,
            ).first()
            can_request_approval = pending is None
        except Exception:
            can_request_approval = False

    return render_template(
        "timer/view_timer.html",
        timer=timer,
        link_templates_by_field=link_templates_by_field,
        time_approvals_enabled=time_approvals_enabled,
        can_request_approval=can_request_approval,
    )


@timer_bp.route("/timer/delete/<int:timer_id>", methods=["POST"])
@login_required
def delete_timer(timer_id):
    """Delete a timer entry"""
    timer = TimeEntry.query.get_or_404(timer_id)

    # Check if user can delete this timer
    if timer.user_id != current_user.id and not current_user.is_admin:
        flash(_("You can only delete your own timers"), "error")
        return redirect(url_for("main.dashboard"))

    # Don't allow deletion of active timers
    if timer.is_active:
        flash(_("Cannot delete an active timer"), "error")
        return redirect(url_for("main.dashboard"))

    # Get the name for the success message (project or client)
    if timer.project:
        target_name = timer.project.name
    elif timer.client:
        target_name = timer.client.name
    else:
        target_name = _("Unknown")

    # Capture entry info for logging before deletion
    entry_id = timer.id
    duration_formatted = timer.duration_formatted
    project_name = timer.project.name if timer.project else None
    client_name = timer.client.name if timer.client else None
    entity_name = project_name or client_name or _("Unknown")
    timer_user_id = timer.user_id  # Capture user_id before deletion

    # Check if time_entry_approvals table exists before deletion
    # This prevents errors when the table doesn't exist but the relationship is defined
    inspector = inspect(db.engine)
    approvals_table_exists = "time_entry_approvals" in inspector.get_table_names()

    # If the approvals table exists, manually delete related approvals first
    # to avoid SQLAlchemy trying to query a non-existent table
    if approvals_table_exists:
        try:
            # Delete related approvals if they exist
            from app.models.time_entry_approval import TimeEntryApproval

            TimeEntryApproval.query.filter_by(time_entry_id=entry_id).delete()
        except Exception as e:
            current_app.logger.warning(f"Could not delete related approvals for time entry {entry_id}: {e}")
            # Continue with deletion anyway

    # If the approvals table doesn't exist, we need to prevent SQLAlchemy from
    # trying to query the relationship. We'll expunge the object and use a direct delete.
    if not approvals_table_exists:
        try:
            # Expunge the object from the session to prevent relationship queries
            db.session.expunge(timer)
            # Use a direct SQL delete to avoid relationship queries
            db.session.execute(text("DELETE FROM time_entries WHERE id = :id"), {"id": entry_id})
        except Exception as e:
            current_app.logger.error(f"Error deleting time entry {entry_id} with direct SQL: {e}")
            flash(_("Could not delete timer due to a database error. Please check server logs."), "error")
            return redirect(url_for("main.dashboard"))
    else:
        # Normal deletion path when the table exists
        db.session.delete(timer)

    if not safe_commit("delete_timer", {"timer_id": entry_id}):
        flash(_("Could not delete timer due to a database error. Please check server logs."), "error")
        return redirect(url_for("main.dashboard"))

    # Invalidate dashboard cache for the timer owner so changes appear immediately
    try:
        from app.utils.cache import invalidate_dashboard_for_user

        invalidate_dashboard_for_user(timer_user_id)
        current_app.logger.debug("Invalidated dashboard cache for user %s after timer deletion", timer_user_id)
    except Exception as e:
        current_app.logger.warning("Failed to invalidate dashboard cache: %s", e)

    # Log activity
    Activity.log(
        user_id=current_user.id,
        action="deleted",
        entity_type="time_entry",
        entity_id=entry_id,
        entity_name=entity_name,
        description=f"Deleted time entry for {entity_name} - {duration_formatted}",
        extra_data={"project_name": project_name, "client_name": client_name, "duration_formatted": duration_formatted},
        ip_address=request.remote_addr,
        user_agent=request.headers.get("User-Agent"),
    )

    # Invalidate dashboard cache so deleted entry disappears immediately
    try:
        from app.utils.cache import invalidate_dashboard_for_user

        invalidate_dashboard_for_user(current_user.id)
        current_app.logger.debug("Invalidated dashboard cache for user %s after deleting timer", current_user.id)
    except Exception as e:
        current_app.logger.warning("Failed to invalidate dashboard cache: %s", e)

    flash(f"Timer for {target_name} deleted successfully", "success")

    # Add cache-busting parameter to ensure fresh page load
    import time

    dashboard_url = url_for("main.dashboard")
    separator = "&" if "?" in dashboard_url else "?"
    redirect_url = f"{dashboard_url}{separator}_refresh={int(time.time())}"
    return redirect(redirect_url)


@timer_bp.route("/time-entries/bulk-delete", methods=["POST"])
@login_required
def bulk_delete_time_entries():
    """Bulk delete time entries"""
    from app.services import TimeTrackingService

    entry_ids = request.form.getlist("entry_ids[]")
    reason = request.form.get("reason", "").strip() or None  # Optional reason for bulk deletion

    if not entry_ids:
        flash(_("No time entries selected"), "warning")
        return redirect(url_for("timer.time_entries_overview"))

    # Load entries
    entry_ids_int = [int(eid) for eid in entry_ids if eid.isdigit()]
    if not entry_ids_int:
        flash(_("Invalid entry IDs"), "error")
        return redirect(url_for("timer.time_entries_overview"))

    entries = TimeEntry.query.filter(TimeEntry.id.in_(entry_ids_int)).all()

    if not entries:
        flash(_("No time entries found"), "error")
        return redirect(url_for("timer.time_entries_overview"))

    # Permission check
    can_view_all = current_user.is_admin or current_user.has_permission("view_all_time_entries")
    deleted_count = 0
    skipped_count = 0

    # Use service layer for proper audit logging
    service = TimeTrackingService()

    for entry in entries:
        # Check permissions
        if not can_view_all and entry.user_id != current_user.id:
            skipped_count += 1
            continue

        # Don't allow deletion of active timers
        if entry.is_active:
            skipped_count += 1
            continue

        # Delete using service layer to get enhanced audit logging
        result = service.delete_entry(
            user_id=current_user.id,
            entry_id=entry.id,
            is_admin=current_user.is_admin,
            reason=reason,  # Use same reason for all entries in bulk delete
        )

        if result.get("success"):
            deleted_count += 1
        else:
            skipped_count += 1

    if deleted_count > 0:
        flash(_("Successfully deleted %(count)d time entry/entries", count=deleted_count), "success")

    if skipped_count > 0:
        flash(_("Skipped %(count)d time entry/entries (no permission or active timer)", count=skipped_count), "warning")

    # Track event
    track_event(current_user.id, "time_entries.bulk_delete", {"count": deleted_count})

    # Preserve filters in redirect
    redirect_url = url_for("timer.time_entries_overview")
    filters = {}
    for key in ["user_id", "project_id", "client_id", "start_date", "end_date", "paid", "billable", "search", "page"]:
        value = request.form.get(key) or request.args.get(key)
        if value:
            filters[key] = value

    if filters:
        redirect_url += "?" + "&".join(f"{k}={v}" for k, v in filters.items())

    return redirect(redirect_url)


@timer_bp.route("/timer/manual", methods=["GET", "POST"])
@login_required
def manual_entry():
    """Create a manual time entry"""
    from app.models import Client
    from app.services import TimeTrackingService
    from app.utils.client_lock import enforce_locked_client_id, get_locked_client_id

    # Get active projects and clients for dropdown (scoped for subcontractors)
    from app.utils.scope_filter import apply_client_scope_to_model, apply_project_scope_to_model

    projects_query = Project.query.filter_by(status="active").order_by(Project.name)
    scope_p = apply_project_scope_to_model(Project, current_user)
    if scope_p is not None:
        projects_query = projects_query.filter(scope_p)
    active_projects = projects_query.all()
    clients_query = Client.query.filter_by(status="active").order_by(Client.name)
    scope_c = apply_client_scope_to_model(Client, current_user)
    if scope_c is not None:
        clients_query = clients_query.filter(scope_c)
    active_clients = clients_query.all()
    only_one_client = len(active_clients) == 1
    single_client = active_clients[0] if only_one_client else None

    # Get project_id, client_id, and task_id from query parameters for pre-filling
    project_id = request.args.get("project_id", type=int)
    client_id = request.args.get("client_id", type=int)
    client_id = enforce_locked_client_id(client_id)
    task_id = request.args.get("task_id", type=int)
    template_id = request.args.get("template", type=int)

    # Load template data if template_id is provided
    template_data = None
    if template_id:
        from app.models import TimeEntryTemplate

        template = TimeEntryTemplate.query.filter_by(id=template_id, user_id=current_user.id).first()
        if template:
            template_data = {
                "project_id": template.project_id,
                "task_id": template.task_id,
                "notes": template.default_notes,
                "tags": template.tags,
                "billable": template.billable,
            }
            # Override with template values if not explicitly set
            if not project_id and template.project_id:
                project_id = template.project_id
            if not task_id and template.task_id:
                task_id = template.task_id

    if request.method == "POST":
        from app.utils.validation import sanitize_input

        project_id = request.form.get("project_id", type=int) or None
        client_id = request.form.get("client_id", type=int) or None
        client_id = enforce_locked_client_id(client_id)
        task_id = request.form.get("task_id", type=int) or None
        start_date = request.form.get("start_date")
        start_time = request.form.get("start_time")
        end_date = request.form.get("end_date")
        end_time = request.form.get("end_time")
        worked_time = (request.form.get("worked_time") or "").strip()
        worked_time_mode = (request.form.get("worked_time_mode") or "").strip()  # 'explicit' when user typed duration
        break_time = (request.form.get("break_time") or "").strip()
        notes = sanitize_input(request.form.get("notes", "").strip(), max_length=2000)
        tags = sanitize_input(request.form.get("tags", "").strip(), max_length=500)
        billable = request.form.get("billable") == "on"

        def _parse_worked_time_minutes(raw: str):
            s = (raw or "").strip()
            if not s:
                return None
            import re

            m = re.match(r"^(\d{1,3}):([0-5]\d)$", s)
            if not m:
                return None
            hours = int(m.group(1))
            minutes = int(m.group(2))
            total = hours * 60 + minutes
            return total if total > 0 else None

        worked_minutes = _parse_worked_time_minutes(worked_time)
        break_minutes = _parse_worked_time_minutes(break_time)
        break_seconds = (break_minutes * 60) if break_minutes is not None else None

        has_all_times = bool(start_date and start_time and end_date and end_time)
        has_duration = worked_minutes is not None

        # Validate time input: either full start/end, or duration-only.
        if not has_all_times and not has_duration:
            flash(_("Please provide either start/end date+time or a worked time duration (HH:MM)."), "error")
            return render_template(
                "timer/manual_entry.html",
                projects=active_projects,
                clients=active_clients,
                only_one_client=only_one_client,
                single_client=single_client,
                selected_project_id=project_id,
                selected_client_id=client_id,
                selected_task_id=task_id,
                template_data=template_data,
                prefill_notes=notes,
                prefill_tags=tags,
                prefill_billable=billable,
                prefill_start_date=start_date,
                prefill_start_time=start_time,
                prefill_end_date=end_date,
                prefill_end_time=end_time,
                prefill_worked_time=worked_time,
                prefill_worked_time_mode=worked_time_mode,
                prefill_break_time=break_time,
            )

        # Validate that either project or client is selected
        if not project_id and not client_id:
            flash(_("Either a project or a client must be selected"), "error")
            return render_template(
                "timer/manual_entry.html",
                projects=active_projects,
                clients=active_clients,
                only_one_client=only_one_client,
                single_client=single_client,
                selected_project_id=project_id,
                selected_client_id=client_id,
                selected_task_id=task_id,
                template_data=template_data,
                prefill_notes=notes,
                prefill_tags=tags,
                prefill_billable=billable,
                prefill_start_date=start_date,
                prefill_start_time=start_time,
                prefill_end_date=end_date,
                prefill_end_time=end_time,
                prefill_worked_time=worked_time,
                prefill_worked_time_mode=worked_time_mode,
                prefill_break_time=break_time,
            )

        # If a locked client is configured, ensure selected project matches it.
        locked_id = get_locked_client_id()
        if locked_id and project_id:
            project = _project_service.get_by_id(project_id)
            if project and getattr(project, "client_id", None) and int(project.client_id) != int(locked_id):
                flash(_("Selected project does not match the locked client."), "error")
                return redirect(url_for("timer.manual_entry"))

        duration_seconds_override = None

        # Parse datetime: treat form input as user's local time, store in app timezone.
        # If duration + start date/time are provided: end = start + duration.
        # If duration only (no start): end=now, start=end-duration.
        # Break is subtracted from span to get worked duration.
        from datetime import timedelta

        try:
            if has_all_times:
                start_time_parsed = parse_user_local_datetime(start_date, start_time, current_user)
                end_time_parsed = parse_user_local_datetime(end_date, end_time, current_user)
                if worked_time_mode == "explicit" and has_duration:
                    duration_seconds_override = worked_minutes * 60
                # When we have start/end and break, we pass break_seconds and do not override duration;
                # calculate_duration() will compute (end - start) - break_seconds
            elif has_duration and start_date and start_time:
                # Combined: worked time + start date/time (user can set date and duration)
                start_time_parsed = parse_user_local_datetime(start_date, start_time, current_user)
                end_time_parsed = start_time_parsed + timedelta(minutes=worked_minutes)
                duration_seconds_override = worked_minutes * 60
            else:
                # Duration-only: no start given → end=now, start=end-duration
                from app.models.time_entry import local_now as _local_now_db

                end_time_parsed = _local_now_db()
                start_time_parsed = end_time_parsed - timedelta(minutes=worked_minutes)
                duration_seconds_override = worked_minutes * 60
        except ValueError:
            flash(_("Invalid date/time format"), "error")
            return render_template(
                "timer/manual_entry.html",
                projects=active_projects,
                clients=active_clients,
                only_one_client=only_one_client,
                single_client=single_client,
                selected_project_id=project_id,
                selected_client_id=client_id,
                selected_task_id=task_id,
                template_data=template_data,
                prefill_notes=notes,
                prefill_tags=tags,
                prefill_billable=billable,
                prefill_start_date=start_date,
                prefill_start_time=start_time,
                prefill_end_date=end_date,
                prefill_end_time=end_time,
                prefill_worked_time=worked_time,
                prefill_worked_time_mode=worked_time_mode,
                prefill_break_time=break_time,
            )

        # When user entered both duration override and break, net duration = duration - break
        if duration_seconds_override is not None and break_seconds is not None:
            duration_seconds_override = max(0, duration_seconds_override - break_seconds)

        # Validate time range
        if end_time_parsed <= start_time_parsed:
            flash(_("End time must be after start time"), "error")
            return render_template(
                "timer/manual_entry.html",
                projects=active_projects,
                clients=active_clients,
                only_one_client=only_one_client,
                single_client=single_client,
                selected_project_id=project_id,
                selected_client_id=client_id,
                selected_task_id=task_id,
                template_data=template_data,
                prefill_notes=notes,
                prefill_tags=tags,
                prefill_billable=billable,
                prefill_start_date=start_date,
                prefill_start_time=start_time,
                prefill_end_date=end_date,
                prefill_end_time=end_time,
                prefill_worked_time=worked_time,
                prefill_worked_time_mode=worked_time_mode,
                prefill_break_time=break_time,
            )

        # Use service to create entry (handles validation)
        time_tracking_service = TimeTrackingService()
        result = time_tracking_service.create_manual_entry(
            user_id=current_user.id,
            project_id=project_id,
            client_id=client_id,
            start_time=start_time_parsed,
            end_time=end_time_parsed,
            duration_seconds=duration_seconds_override,
            break_seconds=break_seconds,
            task_id=task_id,
            notes=notes if notes else None,
            tags=tags if tags else None,
            billable=billable,
        )

        if not result.get("success"):
            flash(_(result.get("message", "Could not create manual entry")), "error")
            return render_template(
                "timer/manual_entry.html",
                projects=active_projects,
                clients=active_clients,
                only_one_client=only_one_client,
                single_client=single_client,
                selected_project_id=project_id,
                selected_client_id=client_id,
                selected_task_id=task_id,
                template_data=template_data,
                prefill_notes=notes,
                prefill_tags=tags,
                prefill_billable=billable,
                prefill_start_date=start_date,
                prefill_start_time=start_time,
                prefill_end_date=end_date,
                prefill_end_time=end_time,
                prefill_worked_time=worked_time,
                prefill_worked_time_mode=worked_time_mode,
                prefill_break_time=break_time,
            )

        entry = result.get("entry")

        # Create success message
        if entry:
            if entry.project:
                target_name = entry.project.name
            elif entry.client:
                target_name = entry.client.name
            else:
                target_name = "Unknown"

            if task_id and entry.project:
                task = Task.query.get(task_id)
                task_name = task.name if task else "Unknown Task"
                flash(
                    _("Manual entry created for %(project)s - %(task)s", project=target_name, task=task_name), "success"
                )
            else:
                flash(_("Manual entry created for %(target)s", target=target_name), "success")

            # Log activity
            entity_name = entry.project.name if entry.project else (entry.client.name if entry.client else "Unknown")
            task_name = entry.task.name if entry.task else None
            duration_formatted = entry.duration_formatted if hasattr(entry, "duration_formatted") else "0:00"

            Activity.log(
                user_id=current_user.id,
                action="created",
                entity_type="time_entry",
                entity_id=entry.id,
                entity_name=f"{entity_name}" + (f" - {task_name}" if task_name else ""),
                description=f"Created time entry for {entity_name}"
                + (f" - {task_name}" if task_name else "")
                + f" - {duration_formatted}",
                extra_data={
                    "project_name": entry.project.name if entry.project else None,
                    "client_name": entry.client.name if entry.client else None,
                    "task_name": task_name,
                    "duration_formatted": duration_formatted,
                    "duration_hours": entry.duration_hours if hasattr(entry, "duration_hours") else None,
                },
                ip_address=request.remote_addr,
                user_agent=request.headers.get("User-Agent"),
            )

        # Invalidate dashboard cache so new entry appears immediately
        try:
            from app.utils.cache import invalidate_dashboard_for_user

            invalidate_dashboard_for_user(current_user.id)
            current_app.logger.debug(
                "Invalidated dashboard cache for user %s after manual entry creation", current_user.id
            )
        except Exception as e:
            current_app.logger.warning("Failed to invalidate dashboard cache: %s", e)

        return redirect(url_for("main.dashboard"))

    # Pre-fill start/end date with today in user's timezone (Issue #489)
    from app.utils.timezone import now_in_user_timezone

    today_local = now_in_user_timezone(current_user)
    today_str = today_local.strftime("%Y-%m-%d")

    return render_template(
        "timer/manual_entry.html",
        projects=active_projects,
        clients=active_clients,
        only_one_client=only_one_client,
        single_client=single_client,
        selected_project_id=project_id,
        selected_client_id=client_id,
        selected_task_id=task_id,
        template_data=template_data,
        prefill_start_date=today_str,
        prefill_end_date=today_str,
    )


@timer_bp.route("/timer/manual/<int:project_id>")
@login_required
def manual_entry_for_project(project_id):
    """Create a manual time entry for a specific project"""
    from app.models import Client

    task_id = request.args.get("task_id", type=int)

    # Check if project exists and is active
    project = Project.query.filter_by(id=project_id, status="active").first()
    if not project:
        flash("Invalid project selected", "error")
        return redirect(url_for("main.dashboard"))

    # Get active projects and clients for dropdown
    active_projects = Project.query.filter_by(status="active").order_by(Project.name).all()
    active_clients = Client.query.filter_by(status="active").order_by(Client.name).all()
    only_one_client = len(active_clients) == 1
    single_client = active_clients[0] if only_one_client else None

    from app.utils.timezone import now_in_user_timezone

    today_local = now_in_user_timezone(current_user)
    today_str = today_local.strftime("%Y-%m-%d")

    return render_template(
        "timer/manual_entry.html",
        projects=active_projects,
        clients=active_clients,
        only_one_client=only_one_client,
        single_client=single_client,
        selected_project_id=project_id,
        selected_task_id=task_id,
        prefill_start_date=today_str,
        prefill_end_date=today_str,
    )


@timer_bp.route("/timer/bulk", methods=["GET", "POST"])
@login_required
def bulk_entry():
    """Create bulk time entries for multiple days"""
    # Get active projects for dropdown
    active_projects = Project.query.filter_by(status="active").order_by(Project.name).all()

    # Get project_id and task_id from query parameters for pre-filling
    project_id = request.args.get("project_id", type=int)
    task_id = request.args.get("task_id", type=int)

    if request.method == "POST":
        project_id = request.form.get("project_id", type=int)
        task_id = request.form.get("task_id", type=int)
        start_date = request.form.get("start_date")
        end_date = request.form.get("end_date")
        start_time = request.form.get("start_time")
        end_time = request.form.get("end_time")
        notes = request.form.get("notes", "").strip()
        tags = request.form.get("tags", "").strip()
        billable = request.form.get("billable") == "on"
        skip_weekends = request.form.get("skip_weekends") == "on"

        # Validate required fields
        if not all([project_id, start_date, end_date, start_time, end_time]):
            flash(_("All fields are required"), "error")
            return render_template(
                "timer/bulk_entry.html",
                projects=active_projects,
                selected_project_id=project_id,
                selected_task_id=task_id,
            )

        # Check if project exists
        project = _project_service.get_by_id(project_id)
        if not project:
            flash(_("Invalid project selected"), "error")
            return render_template(
                "timer/bulk_entry.html",
                projects=active_projects,
                selected_project_id=project_id,
                selected_task_id=task_id,
            )

        # Check if project is active (not archived or inactive)
        if project.status == "archived":
            flash(_("Cannot create time entries for an archived project. Please unarchive the project first."), "error")
            return render_template(
                "timer/bulk_entry.html",
                projects=active_projects,
                selected_project_id=project_id,
                selected_task_id=task_id,
            )
        elif project.status != "active":
            flash(_("Cannot create time entries for an inactive project"), "error")
            return render_template(
                "timer/bulk_entry.html",
                projects=active_projects,
                selected_project_id=project_id,
                selected_task_id=task_id,
            )

        # Validate task if provided
        if task_id:
            task = Task.query.filter_by(id=task_id, project_id=project_id).first()
            if not task:
                flash(_("Invalid task selected"), "error")
                return render_template(
                    "timer/bulk_entry.html",
                    projects=active_projects,
                    selected_project_id=project_id,
                    selected_task_id=task_id,
                )

        # Parse and validate dates
        try:
            from datetime import datetime, timedelta

            start_date_obj = datetime.strptime(start_date, "%Y-%m-%d").date()
            end_date_obj = datetime.strptime(end_date, "%Y-%m-%d").date()

            if end_date_obj < start_date_obj:
                flash(_("End date must be after or equal to start date"), "error")
                return render_template(
                    "timer/bulk_entry.html",
                    projects=active_projects,
                    selected_project_id=project_id,
                    selected_task_id=task_id,
                )

            # Check for reasonable date range (max 31 days)
            if (end_date_obj - start_date_obj).days > 31:
                flash(_("Date range cannot exceed 31 days"), "error")
                return render_template(
                    "timer/bulk_entry.html",
                    projects=active_projects,
                    selected_project_id=project_id,
                    selected_task_id=task_id,
                )
        except ValueError:
            flash(_("Invalid date format"), "error")
            return render_template(
                "timer/bulk_entry.html",
                projects=active_projects,
                selected_project_id=project_id,
                selected_task_id=task_id,
            )

        # Parse and validate times
        try:
            start_time_obj = datetime.strptime(start_time, "%H:%M").time()
            end_time_obj = datetime.strptime(end_time, "%H:%M").time()

            if end_time_obj <= start_time_obj:
                flash("End time must be after start time", "error")
                return render_template(
                    "timer/bulk_entry.html",
                    projects=active_projects,
                    selected_project_id=project_id,
                    selected_task_id=task_id,
                )
        except ValueError:
            flash(_("Invalid time format"), "error")
            return render_template(
                "timer/bulk_entry.html",
                projects=active_projects,
                selected_project_id=project_id,
                selected_task_id=task_id,
            )

        # Generate date range
        current_date = start_date_obj
        dates_to_create = []

        while current_date <= end_date_obj:
            # Skip weekends if requested
            if skip_weekends and current_date.weekday() >= 5:  # Saturday = 5, Sunday = 6
                current_date += timedelta(days=1)
                continue

            dates_to_create.append(current_date)
            current_date += timedelta(days=1)

        if not dates_to_create:
            flash(_("No valid dates found in the selected range"), "error")
            return render_template(
                "timer/bulk_entry.html",
                projects=active_projects,
                selected_project_id=project_id,
                selected_task_id=task_id,
            )

        # Check for existing entries on the same dates/times
        from app.models.time_entry import local_now

        existing_entries = []

        for date_obj in dates_to_create:
            start_datetime = datetime.combine(date_obj, start_time_obj)
            end_datetime = datetime.combine(date_obj, end_time_obj)

            # Check for overlapping entries
            overlapping = TimeEntry.query.filter(
                TimeEntry.user_id == current_user.id,
                TimeEntry.start_time <= end_datetime,
                TimeEntry.end_time >= start_datetime,
                TimeEntry.end_time.isnot(None),
            ).first()

            if overlapping:
                existing_entries.append(date_obj.strftime("%Y-%m-%d"))

        if existing_entries:
            flash(
                f'Time entries already exist for these dates: {", ".join(existing_entries[:5])}{"..." if len(existing_entries) > 5 else ""}',
                "error",
            )
            return render_template(
                "timer/bulk_entry.html",
                projects=active_projects,
                selected_project_id=project_id,
                selected_task_id=task_id,
            )

        # Create bulk entries
        created_entries = []

        try:
            for date_obj in dates_to_create:
                start_datetime = datetime.combine(date_obj, start_time_obj)
                end_datetime = datetime.combine(date_obj, end_time_obj)

                entry = TimeEntry(
                    user_id=current_user.id,
                    project_id=project_id,
                    task_id=task_id,
                    start_time=start_datetime,
                    end_time=end_datetime,
                    notes=notes,
                    tags=tags,
                    source="manual",
                    billable=billable,
                )

                db.session.add(entry)
                created_entries.append(entry)

            if not safe_commit(
                "bulk_entry", {"user_id": current_user.id, "project_id": project_id, "count": len(created_entries)}
            ):
                flash(_("Could not create bulk entries due to a database error. Please check server logs."), "error")
                return render_template(
                    "timer/bulk_entry.html",
                    projects=active_projects,
                    selected_project_id=project_id,
                    selected_task_id=task_id,
                )

            task_name = ""
            if task_id:
                task = Task.query.get(task_id)
                task_name = f" - {task.name}" if task else ""

            flash(f"Successfully created {len(created_entries)} time entries for {project.name}{task_name}", "success")
            return redirect(url_for("main.dashboard"))

        except Exception as e:
            db.session.rollback()
            current_app.logger.exception("Error creating bulk entries: %s", e)
            flash(_("An error occurred while creating bulk entries. Please try again."), "error")
            return render_template(
                "timer/bulk_entry.html",
                projects=active_projects,
                selected_project_id=project_id,
                selected_task_id=task_id,
            )

    return render_template(
        "timer/bulk_entry.html", projects=active_projects, selected_project_id=project_id, selected_task_id=task_id
    )


@timer_bp.route("/timer")
@login_required
def timer_page():
    """Dedicated timer page with visual progress ring and quick project selection"""
    active_timer = current_user.active_timer

    # Get active projects and clients for dropdown
    active_projects = Project.query.filter_by(status="active").order_by(Project.name).all()
    active_clients = Client.query.filter_by(status="active").order_by(Client.name).all()
    only_one_client = len(active_clients) == 1
    single_client = active_clients[0] if only_one_client else None

    # Get recent projects (projects used in last 30 days)
    thirty_days_ago = datetime.utcnow() - timedelta(days=30)
    recent_project_ids = (
        db.session.query(TimeEntry.project_id)
        .filter(
            TimeEntry.user_id == current_user.id,
            TimeEntry.start_time >= thirty_days_ago,
            TimeEntry.end_time.isnot(None),
        )
        .group_by(TimeEntry.project_id)
        .order_by(db.func.max(TimeEntry.start_time).desc())
        .limit(5)
        .all()
    )

    recent_project_ids_list = [pid[0] for pid in recent_project_ids]
    if recent_project_ids_list:
        # Create a dict to preserve order from recent_project_ids_list
        order_map = {pid: idx for idx, pid in enumerate(recent_project_ids_list)}
        recent_projects = Project.query.filter(
            Project.id.in_(recent_project_ids_list), Project.status == "active"
        ).all()
        # Sort by order in recent_project_ids_list
        recent_projects.sort(key=lambda p: order_map.get(p.id, 999))
    else:
        recent_projects = []

    # Get tasks for active timer's project if timer is active
    tasks = []
    if active_timer and active_timer.project_id:
        tasks = (
            Task.query.filter(
                Task.project_id == active_timer.project_id, Task.status.in_(["todo", "in_progress", "review"])
            )
            .order_by(Task.name)
            .all()
        )

    # Get user's time entry templates (most recently used first)
    from sqlalchemy import desc
    from sqlalchemy.orm import joinedload

    from app.models import TimeEntryTemplate

    templates = (
        TimeEntryTemplate.query.options(joinedload(TimeEntryTemplate.project), joinedload(TimeEntryTemplate.task))
        .filter_by(user_id=current_user.id)
        .order_by(desc(TimeEntryTemplate.last_used_at))
        .limit(5)
        .all()
    )

    return render_template(
        "timer/timer_page.html",
        active_timer=active_timer,
        projects=active_projects,
        clients=active_clients,
        only_one_client=only_one_client,
        single_client=single_client,
        recent_projects=recent_projects,
        tasks=tasks,
        templates=templates,
    )


@timer_bp.route("/timer/calendar")
@login_required
def calendar_view():
    """Calendar UI combining day/week/month with list toggle."""
    # Provide projects for quick assignment during drag-create
    active_projects = Project.query.filter_by(status="active").order_by(Project.name).all()
    return render_template("timer/calendar.html", projects=active_projects)


@timer_bp.route("/timer/bulk/<int:project_id>")
@login_required
def bulk_entry_for_project(project_id):
    """Create bulk time entries for a specific project"""
    task_id = request.args.get("task_id", type=int)

    # Check if project exists and is active
    project = Project.query.filter_by(id=project_id, status="active").first()
    if not project:
        flash("Invalid project selected", "error")
        return redirect(url_for("main.dashboard"))

    # Get active projects for dropdown
    active_projects = Project.query.filter_by(status="active").order_by(Project.name).all()

    return render_template(
        "timer/bulk_entry.html", projects=active_projects, selected_project_id=project_id, selected_task_id=task_id
    )


@timer_bp.route("/timer/duplicate/<int:timer_id>")
@login_required
def duplicate_timer(timer_id):
    """Duplicate an existing time entry - opens manual entry form with pre-filled data"""
    from app.models import Client

    timer = TimeEntry.query.get_or_404(timer_id)

    # Check if user can duplicate this timer
    if timer.user_id != current_user.id and not current_user.is_admin:
        flash(_("You can only duplicate your own timers"), "error")
        return redirect(url_for("main.dashboard"))

    # Get active projects and clients for dropdown
    active_projects = Project.query.filter_by(status="active").order_by(Project.name).all()
    active_clients = Client.query.filter_by(status="active").order_by(Client.name).all()
    only_one_client = len(active_clients) == 1
    single_client = active_clients[0] if only_one_client else None

    # Track duplication event
    log_event(
        "timer.duplicated",
        user_id=current_user.id,
        time_entry_id=timer.id,
        project_id=timer.project_id,
        task_id=timer.task_id,
    )
    track_event(
        current_user.id,
        "timer.duplicated",
        {
            "time_entry_id": timer.id,
            "project_id": timer.project_id,
            "task_id": timer.task_id,
            "has_notes": bool(timer.notes),
            "has_tags": bool(timer.tags),
        },
    )

    # Render the manual entry form with pre-filled data
    break_sec = getattr(timer, "break_seconds", None) or 0
    prefill_break = f"{break_sec // 3600}:{(break_sec % 3600) // 60:02d}" if break_sec else ""
    return render_template(
        "timer/manual_entry.html",
        projects=active_projects,
        clients=active_clients,
        only_one_client=only_one_client,
        single_client=single_client,
        selected_project_id=timer.project_id,
        selected_client_id=timer.client_id,
        selected_task_id=timer.task_id,
        prefill_notes=timer.notes,
        prefill_tags=timer.tags,
        prefill_billable=timer.billable,
        prefill_break_time=prefill_break,
        is_duplicate=True,
        original_entry=timer,
    )


@timer_bp.route("/timer/resume/<int:timer_id>", endpoint="resume_timer_by_id")
@login_required
def resume_timer_by_id(timer_id):
    """Resume an existing time entry - starts a new active timer with same properties"""
    timer = TimeEntry.query.get_or_404(timer_id)

    # Check if user can resume this timer
    if timer.user_id != current_user.id and not current_user.is_admin:
        flash(_("You can only resume your own timers"), "error")
        return redirect(url_for("main.dashboard"))

    can_start, _unused = TimeTrackingService().can_start_timer(current_user.id)
    if not can_start:
        flash("You already have an active timer. Stop it before resuming another one.", "error")
        current_app.logger.info("Resume timer blocked: user already has an active timer")
        return redirect(url_for("main.dashboard"))

    project = None
    client = None
    project_id = None
    client_id = None

    # Check if timer is linked to a project or client
    if timer.project_id:
        # Timer is linked to a project
        project = _project_service.get_by_id(timer.project_id)
        if not project:
            flash(_("Project no longer exists"), "error")
            return redirect(url_for("main.dashboard"))

        if project.status == "archived":
            flash(_("Cannot start timer for an archived project. Please unarchive the project first."), "error")
            return redirect(url_for("main.dashboard"))
        elif project.status != "active":
            flash(_("Cannot start timer for an inactive project"), "error")
            return redirect(url_for("main.dashboard"))

        project_id = timer.project_id

        # Validate task if it exists
        if timer.task_id:
            task = Task.query.filter_by(id=timer.task_id, project_id=timer.project_id).first()
            if not task:
                # Task was deleted, continue without it
                task_id = None
            else:
                task_id = timer.task_id
        else:
            task_id = None
    elif timer.client_id:
        # Timer is linked to a client
        client = Client.query.filter_by(id=timer.client_id, status="active").first()
        if not client:
            flash(_("Client no longer exists or is inactive"), "error")
            return redirect(url_for("main.dashboard"))

        client_id = timer.client_id
        task_id = None  # Tasks are not allowed for client-only timers
    else:
        flash(_("Timer is not linked to a project or client"), "error")
        return redirect(url_for("main.dashboard"))

    # Create new timer with copied properties
    from app.models.time_entry import local_now

    new_timer = TimeEntry(
        user_id=current_user.id,
        project_id=project_id,
        client_id=client_id,
        task_id=task_id,
        start_time=local_now(),
        notes=timer.notes,
        tags=timer.tags,
        source="auto",
        billable=timer.billable,
    )

    db.session.add(new_timer)
    if not safe_commit(
        "resume_timer",
        {
            "user_id": current_user.id,
            "original_timer_id": timer_id,
            "project_id": project_id,
            "client_id": client_id,
        },
    ):
        flash(_("Could not resume timer due to a database error. Please check server logs."), "error")
        return redirect(url_for("main.dashboard"))

    current_app.logger.info(
        "Resumed timer id=%s from original timer=%s for user=%s project_id=%s client_id=%s",
        new_timer.id,
        timer_id,
        current_user.username,
        project_id,
        client_id,
    )

    # Track timer resumed event
    log_event(
        "timer.resumed",
        user_id=current_user.id,
        time_entry_id=new_timer.id,
        original_timer_id=timer_id,
        project_id=project_id,
        client_id=client_id,
        task_id=task_id,
        description=timer.notes,
    )
    track_event(
        current_user.id,
        "timer.resumed",
        {
            "time_entry_id": new_timer.id,
            "original_timer_id": timer_id,
            "project_id": project_id,
            "client_id": client_id,
            "task_id": task_id,
            "has_notes": bool(timer.notes),
            "has_tags": bool(timer.tags),
        },
    )

    # Log activity
    if project:
        project_name = project.name
        task = Task.query.get(task_id) if task_id else None
        task_name = task.name if task else None
        entity_name = f"{project_name}" + (f" - {task_name}" if task_name else "")
        description = f"Resumed timer for {project_name}" + (f" - {task_name}" if task_name else "")
    elif client:
        client_name = client.name
        entity_name = client_name
        description = f"Resumed timer for {client_name}"
        task_name = None
    else:
        entity_name = _("Unknown")
        description = _("Resumed timer")
        task_name = None

    Activity.log(
        user_id=current_user.id,
        action="started",
        entity_type="time_entry",
        entity_id=new_timer.id,
        entity_name=entity_name,
        description=description,
        extra_data={"project_id": project_id, "client_id": client_id, "task_id": task_id, "resumed_from": timer_id},
        ip_address=request.remote_addr,
        user_agent=request.headers.get("User-Agent"),
    )

    # Emit WebSocket event for real-time updates
    try:
        payload = {
            "user_id": current_user.id,
            "timer_id": new_timer.id,
            "start_time": new_timer.start_time.isoformat(),
        }
        if project:
            payload["project_name"] = project.name
        if client:
            payload["client_name"] = client.name
        if task_id:
            task = Task.query.get(task_id)
            if task:
                payload["task_id"] = task_id
                payload["task_name"] = task.name
        socketio.emit("timer_started", payload)
    except Exception as e:
        current_app.logger.warning("Socket emit failed for timer_resumed: %s", e)

    # Invalidate dashboard cache so timer appears immediately
    try:
        from app.utils.cache import invalidate_dashboard_for_user

        invalidate_dashboard_for_user(current_user.id)
        current_app.logger.debug("Invalidated dashboard cache for user %s", current_user.id)
    except Exception as e:
        current_app.logger.warning("Failed to invalidate dashboard cache: %s", e)

    # Create success message
    if project:
        if task_name:
            flash(f"Timer resumed for {project_name} - {task_name}", "success")
        else:
            flash(f"Timer resumed for {project_name}", "success")
    elif client:
        flash(f"Timer resumed for {client_name}", "success")
    else:
        flash(_("Timer resumed"), "success")

    return redirect(url_for("main.dashboard"))


@timer_bp.route("/time-entries")
@login_required
def time_entries_overview():
    """Overview page showing all time entries with filters and bulk actions"""
    from sqlalchemy import desc, func, or_
    from sqlalchemy.orm import joinedload

    from app.repositories import ProjectRepository, TimeEntryRepository, UserRepository
    from app.utils.client_lock import enforce_locked_client_id

    # Get filter parameters
    user_id = request.args.get("user_id", type=int)
    project_id = request.args.get("project_id", type=int)
    client_id = request.args.get("client_id", type=int)
    client_id = enforce_locked_client_id(client_id)
    start_date = request.args.get("start_date", "")
    end_date = request.args.get("end_date", "")
    paid_filter = request.args.get("paid", "")  # "true", "false", or ""
    billable_filter = request.args.get("billable", "")  # "true", "false", or ""
    search = request.args.get("search", "").strip()
    page = request.args.get("page", 1, type=int)
    per_page = request.args.get("per_page", 50, type=int)

    # Get custom field filters for clients
    # Format: custom_field_<field_key>=value
    client_custom_field = {}
    from app.models import CustomFieldDefinition

    active_definitions = CustomFieldDefinition.get_active_definitions()
    for definition in active_definitions:
        field_value = request.args.get(f"custom_field_{definition.field_key}", "").strip()
        if field_value:
            client_custom_field[definition.field_key] = field_value

    # Permission check: can user view all entries?
    can_view_all = current_user.is_admin or current_user.has_permission("view_all_time_entries")

    # Build query with eager loading to avoid N+1 queries
    query = TimeEntry.query.options(
        joinedload(TimeEntry.user),
        joinedload(TimeEntry.project),
        joinedload(TimeEntry.client),
        joinedload(TimeEntry.task),
    ).filter(
        # Completed entries OR duration-only entries (duration_seconds set but end_time missing).
        # This keeps duration-only manual logs visible even if end_time is absent for any reason.
        or_(
            TimeEntry.end_time.isnot(None),
            db.and_(TimeEntry.duration_seconds.isnot(None), TimeEntry.source == TimeEntrySource.MANUAL.value),
        )
    )

    # Filter by user
    if user_id:
        if can_view_all:
            query = query.filter(TimeEntry.user_id == user_id)
        elif user_id == current_user.id:
            query = query.filter(TimeEntry.user_id == current_user.id)
        else:
            flash(_("You do not have permission to view other users' time entries"), "error")
            return redirect(url_for("timer.time_entries_overview"))
    elif not can_view_all:
        # Non-admin users can only see their own entries
        query = query.filter(TimeEntry.user_id == current_user.id)

    # Filter by project
    if project_id:
        query = query.filter(TimeEntry.project_id == project_id)

    # Filter by client
    if client_id:
        query = query.filter(TimeEntry.client_id == client_id)

    # Filter by client custom fields
    if client_custom_field:
        # Join Client table to filter by custom fields
        query = query.join(Client, TimeEntry.client_id == Client.id)

        # Determine database type for custom field filtering
        is_postgres = False
        try:
            from sqlalchemy import inspect

            engine = db.engine
            is_postgres = "postgresql" in str(engine.url).lower()
        except Exception as e:
            # Log but continue - database type detection failure is not critical
            current_app.logger.debug(f"Failed to detect database type: {e}")

        # Build custom field filter conditions
        custom_field_conditions = []
        for field_key, field_value in client_custom_field.items():
            if not field_key or not field_value:
                continue

            if is_postgres:
                # PostgreSQL: Use JSONB operators
                try:
                    from sqlalchemy import String, cast

                    # Match exact value in custom_fields JSONB
                    custom_field_conditions.append(
                        db.cast(Client.custom_fields[field_key].astext, String) == str(field_value)
                    )
                except Exception as e:
                    # Fallback to Python filtering if JSONB fails
                    current_app.logger.debug(
                        f"JSONB filtering failed for field {field_key}, will use Python filtering: {e}"
                    )

        if custom_field_conditions:
            query = query.filter(db.or_(*custom_field_conditions))

    # Filter by date range
    if start_date:
        try:
            start_dt = datetime.strptime(start_date, "%Y-%m-%d")
            query = query.filter(TimeEntry.start_time >= start_dt)
        except ValueError:
            pass

    if end_date:
        try:
            end_dt = datetime.strptime(end_date, "%Y-%m-%d")
            # Include the entire end date
            end_dt = end_dt.replace(hour=23, minute=59, second=59)
            query = query.filter(TimeEntry.start_time <= end_dt)
        except ValueError:
            pass

    # Filter by paid status
    if paid_filter == "true":
        query = query.filter(TimeEntry.paid == True)
    elif paid_filter == "false":
        query = query.filter(TimeEntry.paid == False)

    # Filter by billable status
    if billable_filter == "true":
        query = query.filter(TimeEntry.billable == True)
    elif billable_filter == "false":
        query = query.filter(TimeEntry.billable == False)

    # Search in notes and tags
    if search:
        search_pattern = f"%{search}%"
        query = query.filter(or_(TimeEntry.notes.ilike(search_pattern), TimeEntry.tags.ilike(search_pattern)))

    # Order by start time (most recent first)
    query = query.order_by(desc(TimeEntry.start_time))

    # Pagination
    pagination = query.paginate(page=page, per_page=per_page, error_out=False)
    time_entries = pagination.items

    # For SQLite or if JSONB filtering didn't work, filter by custom fields in Python
    if client_custom_field:
        try:
            from sqlalchemy import inspect

            engine = db.engine
            is_postgres = "postgresql" in str(engine.url).lower()

            if not is_postgres:
                # SQLite: Filter in Python
                filtered_entries = []
                for entry in time_entries:
                    if not entry.client:
                        continue

                    # Check if client matches all custom field filters
                    matches = True
                    for field_key, field_value in client_custom_field.items():
                        if not field_key or not field_value:
                            continue

                        client_value = entry.client.custom_fields.get(field_key) if entry.client.custom_fields else None
                        if str(client_value) != str(field_value):
                            matches = False
                            break

                    if matches:
                        filtered_entries.append(entry)

                # Update pagination with filtered results
                time_entries = filtered_entries
                # Recalculate pagination manually
                total = len(filtered_entries)
                start = (page - 1) * per_page
                end = start + per_page
                time_entries = filtered_entries[start:end]

                # Create a pagination-like object
                from flask_sqlalchemy import Pagination

                pagination = Pagination(query=None, page=page, per_page=per_page, total=total, items=time_entries)
        except Exception as e:
            current_app.logger.warning("Time entries list filtering failed, using original results: %s", e)

    # Get filter options
    projects = []
    clients = []
    users = []

    if can_view_all:
        project_repo = ProjectRepository()
        projects = project_repo.get_active_projects()
        clients = Client.query.filter_by(status="active").order_by(Client.name).all()
        user_repo = UserRepository()
        users = user_repo.get_active_users()
    else:
        # For non-admin users, only show their projects
        # Get projects from user's time entries
        time_entry_repo = TimeEntryRepository()
        user_project_ids = time_entry_repo.get_distinct_project_ids_for_user(current_user.id)
        if user_project_ids:
            projects = (
                Project.query.filter(Project.id.in_(user_project_ids), Project.status == "active")
                .order_by(Project.name)
                .all()
            )
            # Get clients from user's projects
            client_ids = set(p.client_id for p in projects if p.client_id)
            if client_ids:
                clients = (
                    Client.query.filter(Client.id.in_(client_ids), Client.status == "active")
                    .order_by(Client.name)
                    .all()
                )
        users = [current_user]

    only_one_client = len(clients) == 1
    single_client = clients[0] if only_one_client else None

    # Calculate totals
    total_hours = sum(entry.duration_hours for entry in time_entries)
    total_billable_hours = sum(entry.duration_hours for entry in time_entries if entry.billable)
    total_paid_hours = sum(entry.duration_hours for entry in time_entries if entry.paid)

    # Track page view
    track_event(
        current_user.id,
        "time_entries_overview.viewed",
        {
            "has_filters": bool(
                user_id or project_id or client_id or start_date or end_date or paid_filter or billable_filter or search
            ),
            "page": page,
            "per_page": per_page,
        },
    )

    filters_dict = {
        "user_id": user_id,
        "project_id": project_id,
        "client_id": client_id,
        "start_date": start_date,
        "end_date": end_date,
        "paid": paid_filter,
        "billable": billable_filter,
        "search": search,
        "client_custom_field": client_custom_field,
        "page": page,
        "per_page": per_page,
    }

    # Build URL-safe filters for url_for (exclude dict and page; expand client_custom_field).
    # Passing client_custom_field (a dict) or page into url_for breaks URL building and can
    # cause 500s. Pagination links pass page explicitly, so we omit it here.
    url_filters = {
        k: v for k, v in filters_dict.items() if k not in ("client_custom_field", "page") and v is not None and v != ""
    }
    for k, v in (filters_dict.get("client_custom_field") or {}).items():
        if v:
            url_filters[f"custom_field_{k}"] = v

    # Get custom field definitions for filter UI
    from app.models import CustomFieldDefinition

    custom_field_definitions = CustomFieldDefinition.get_active_definitions()

    # Get link templates for invoice_number (for clickable values)
    from sqlalchemy.exc import ProgrammingError

    from app.models import LinkTemplate

    link_templates_by_field = {}
    try:
        for template in LinkTemplate.get_active_templates():
            if template.field_key == "invoice_number":
                link_templates_by_field["invoice_number"] = template
    except ProgrammingError as e:
        # Handle case where link_templates table doesn't exist (migration not run)
        if "does not exist" in str(e.orig) or "relation" in str(e.orig).lower():
            current_app.logger.warning("link_templates table does not exist. Run migration: flask db upgrade")
            link_templates_by_field = {}
        else:
            raise

    # Time entry approvals: which entries on this page have a pending approval?
    from app.utils.module_helpers import is_module_enabled

    time_approvals_enabled = is_module_enabled("time_approvals")
    entry_ids_with_pending_approval = set()
    if time_approvals_enabled and time_entries:
        try:
            from app.models.time_entry_approval import ApprovalStatus, TimeEntryApproval

            entry_ids = [e.id for e in time_entries]
            pending = TimeEntryApproval.query.filter(
                TimeEntryApproval.time_entry_id.in_(entry_ids),
                TimeEntryApproval.status == ApprovalStatus.PENDING,
            ).all()
            entry_ids_with_pending_approval = {a.time_entry_id for a in pending}
        except Exception:
            entry_ids_with_pending_approval = set()

    # Check if this is an AJAX request
    if request.headers.get("X-Requested-With") == "XMLHttpRequest":
        # Return only the time entries list HTML for AJAX requests
        from flask import make_response

        response = make_response(
            render_template(
                "timer/_time_entries_list.html",
                time_entries=time_entries,
                pagination=pagination,
                can_view_all=can_view_all,
                filters=filters_dict,
                url_filters=url_filters,
                custom_field_definitions=custom_field_definitions,
                link_templates_by_field=link_templates_by_field,
                time_approvals_enabled=time_approvals_enabled,
                entry_ids_with_pending_approval=entry_ids_with_pending_approval,
            )
        )
        response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
        return response

    return render_template(
        "timer/time_entries_overview.html",
        time_entries=time_entries,
        pagination=pagination,
        projects=projects,
        clients=clients,
        only_one_client=only_one_client,
        single_client=single_client,
        users=users,
        can_view_all=can_view_all,
        filters=filters_dict,
        url_filters=url_filters,
        custom_field_definitions=custom_field_definitions,
        link_templates_by_field=link_templates_by_field,
        time_approvals_enabled=time_approvals_enabled,
        entry_ids_with_pending_approval=entry_ids_with_pending_approval,
        totals={
            "total_hours": round(total_hours, 2),
            "total_billable_hours": round(total_billable_hours, 2),
            "total_paid_hours": round(total_paid_hours, 2),
            "total_entries": len(time_entries),
        },
    )


@timer_bp.route("/time-entries/export/csv")
@login_required
def export_time_entries_csv():
    """Export (filtered) time entries as CSV. Mirrors the /time-entries filters."""
    import csv
    import io

    from flask import abort, send_file
    from sqlalchemy import desc, or_
    from sqlalchemy.orm import joinedload

    from app.utils.client_lock import enforce_locked_client_id

    # Get filter parameters (same as time_entries_overview)
    user_id = request.args.get("user_id", type=int)
    project_id = request.args.get("project_id", type=int)
    client_id = request.args.get("client_id", type=int)
    client_id = enforce_locked_client_id(client_id)
    start_date = request.args.get("start_date", "")
    end_date = request.args.get("end_date", "")
    paid_filter = request.args.get("paid", "")  # "true", "false", or ""
    billable_filter = request.args.get("billable", "")  # "true", "false", or ""
    search = request.args.get("search", "").strip()

    # Custom client-field filters
    client_custom_field = {}
    from app.models import CustomFieldDefinition

    active_definitions = CustomFieldDefinition.get_active_definitions()
    for definition in active_definitions:
        field_value = request.args.get(f"custom_field_{definition.field_key}", "").strip()
        if field_value:
            client_custom_field[definition.field_key] = field_value

    can_view_all = current_user.is_admin or current_user.has_permission("view_all_time_entries")

    query = TimeEntry.query.options(
        joinedload(TimeEntry.user),
        joinedload(TimeEntry.project),
        joinedload(TimeEntry.client),
        joinedload(TimeEntry.task),
    ).filter(
        or_(
            TimeEntry.end_time.isnot(None),
            db.and_(TimeEntry.duration_seconds.isnot(None), TimeEntry.source == TimeEntrySource.MANUAL.value),
        )
    )

    # Permission / user scoping
    if user_id:
        if can_view_all:
            query = query.filter(TimeEntry.user_id == user_id)
        elif user_id == current_user.id:
            query = query.filter(TimeEntry.user_id == current_user.id)
        else:
            abort(403)
    elif not can_view_all:
        query = query.filter(TimeEntry.user_id == current_user.id)

    if project_id:
        query = query.filter(TimeEntry.project_id == project_id)
    if client_id:
        query = query.filter(TimeEntry.client_id == client_id)

    # Client custom field filtering (mirrors overview behavior)
    is_postgres = False
    if client_custom_field:
        query = query.join(Client, TimeEntry.client_id == Client.id)
        try:
            engine = db.engine
            is_postgres = "postgresql" in str(engine.url).lower()
        except Exception as e:
            current_app.logger.debug("Failed to detect database type: %s", e)

        if is_postgres:
            custom_field_conditions = []
            for field_key, field_value in client_custom_field.items():
                if not field_key or not field_value:
                    continue
                try:
                    from sqlalchemy import String, cast

                    custom_field_conditions.append(
                        db.cast(Client.custom_fields[field_key].astext, String) == str(field_value)
                    )
                except Exception as e:
                    current_app.logger.debug(
                        "JSONB filtering failed for field %s, will use Python filtering: %s", field_key, e
                    )
            if custom_field_conditions:
                query = query.filter(db.or_(*custom_field_conditions))

    # Date range
    if start_date:
        try:
            start_dt = datetime.strptime(start_date, "%Y-%m-%d")
            query = query.filter(TimeEntry.start_time >= start_dt)
        except ValueError:
            pass
    if end_date:
        try:
            end_dt = datetime.strptime(end_date, "%Y-%m-%d").replace(hour=23, minute=59, second=59)
            query = query.filter(TimeEntry.start_time <= end_dt)
        except ValueError:
            pass

    # Paid/billable
    if paid_filter == "true":
        query = query.filter(TimeEntry.paid == True)
    elif paid_filter == "false":
        query = query.filter(TimeEntry.paid == False)

    if billable_filter == "true":
        query = query.filter(TimeEntry.billable == True)
    elif billable_filter == "false":
        query = query.filter(TimeEntry.billable == False)

    # Search in notes/tags
    if search:
        search_pattern = f"%{search}%"
        query = query.filter(or_(TimeEntry.notes.ilike(search_pattern), TimeEntry.tags.ilike(search_pattern)))

    query = query.order_by(desc(TimeEntry.start_time))
    entries = query.all()

    # SQLite (or non-JSONB) custom-field filtering fallback (same semantics as overview)
    if client_custom_field and not is_postgres:
        filtered = []
        for entry in entries:
            if not entry.client:
                continue
            matches = True
            for field_key, field_value in client_custom_field.items():
                if not field_key or not field_value:
                    continue
                client_value = entry.client.custom_fields.get(field_key) if entry.client.custom_fields else None
                if str(client_value) != str(field_value):
                    matches = False
                    break
            if matches:
                filtered.append(entry)
        entries = filtered

    # CSV output (null-safe: user/project/client can be missing; wrap in try/except for Docker log visibility)
    try:
        settings = Settings.get_settings()
        delimiter = getattr(settings, "export_delimiter", ",") or ","
        output = io.StringIO()
        writer = csv.writer(output, delimiter=delimiter)

        writer.writerow(
            [
                "ID",
                "User",
                "Project",
                "Client",
                "Task",
                "Start Time",
                "End Time",
                "Duration (hours)",
                "Duration (formatted)",
                "Notes",
                "Tags",
                "Source",
                "Billable",
                "Paid",
                "Created At",
                "Updated At",
            ]
        )

        for entry in entries:
            # Project.client is a property returning the client name string
            client_name = (entry.client.name if entry.client else "") or (entry.project.client if entry.project else "")
            writer.writerow(
                [
                    entry.id,
                    (entry.user.display_name if entry.user else ""),
                    (entry.project.name if entry.project else ""),
                    client_name,
                    (entry.task.name if entry.task else ""),
                    entry.start_time.isoformat() if entry.start_time else "",
                    entry.end_time.isoformat() if entry.end_time else "",
                    getattr(entry, "duration_hours", ""),
                    getattr(entry, "duration_formatted", ""),
                    entry.notes or "",
                    entry.tags or "",
                    entry.source or "",
                    "Yes" if entry.billable else "No",
                    "Yes" if entry.paid else "No",
                    entry.created_at.isoformat() if entry.created_at else "",
                    entry.updated_at.isoformat() if entry.updated_at else "",
                ]
            )

        csv_bytes = output.getvalue().encode("utf-8")

        # Filename includes optional date range
        start_part = start_date or "all"
        end_part = end_date or "all"
        filename = f"time_entries_{start_part}_to_{end_part}.csv"

        return send_file(
            io.BytesIO(csv_bytes),
            mimetype="text/csv",
            as_attachment=True,
            download_name=filename,
        )
    except Exception:
        current_app.logger.exception("CSV export failed (timer.export_time_entries_csv)")
        raise


@timer_bp.route("/time-entries/export/pdf")
@login_required
def export_time_entries_pdf():
    """Export (filtered) time entries as PDF. Mirrors the /time-entries filters."""
    import io

    from flask import abort, send_file
    from sqlalchemy import desc, or_
    from sqlalchemy.orm import joinedload

    from app.utils.client_lock import enforce_locked_client_id

    # Get filter parameters (same as time_entries_overview)
    user_id = request.args.get("user_id", type=int)
    project_id = request.args.get("project_id", type=int)
    client_id = request.args.get("client_id", type=int)
    client_id = enforce_locked_client_id(client_id)
    start_date = request.args.get("start_date", "")
    end_date = request.args.get("end_date", "")
    paid_filter = request.args.get("paid", "")  # "true", "false", or ""
    billable_filter = request.args.get("billable", "")  # "true", "false", or ""
    search = request.args.get("search", "").strip()

    # Custom client-field filters
    client_custom_field = {}
    from app.models import CustomFieldDefinition

    active_definitions = CustomFieldDefinition.get_active_definitions()
    for definition in active_definitions:
        field_value = request.args.get(f"custom_field_{definition.field_key}", "").strip()
        if field_value:
            client_custom_field[definition.field_key] = field_value

    can_view_all = current_user.is_admin or current_user.has_permission("view_all_time_entries")

    query = TimeEntry.query.options(
        joinedload(TimeEntry.user),
        joinedload(TimeEntry.project),
        joinedload(TimeEntry.client),
        joinedload(TimeEntry.task),
    ).filter(
        or_(
            TimeEntry.end_time.isnot(None),
            db.and_(TimeEntry.duration_seconds.isnot(None), TimeEntry.source == TimeEntrySource.MANUAL.value),
        )
    )

    # Permission / user scoping
    if user_id:
        if can_view_all:
            query = query.filter(TimeEntry.user_id == user_id)
        elif user_id == current_user.id:
            query = query.filter(TimeEntry.user_id == current_user.id)
        else:
            abort(403)
    elif not can_view_all:
        query = query.filter(TimeEntry.user_id == current_user.id)

    if project_id:
        query = query.filter(TimeEntry.project_id == project_id)
    if client_id:
        query = query.filter(TimeEntry.client_id == client_id)

    # Client custom field filtering (same as CSV export)
    is_postgres = False
    if client_custom_field:
        query = query.join(Client, TimeEntry.client_id == Client.id)
        try:
            engine = db.engine
            is_postgres = "postgresql" in str(engine.url).lower()
        except Exception as e:
            current_app.logger.debug("Failed to detect database type: %s", e)

        if is_postgres:
            custom_field_conditions = []
            for field_key, field_value in client_custom_field.items():
                if not field_key or not field_value:
                    continue
                try:
                    from sqlalchemy import String, cast

                    custom_field_conditions.append(
                        db.cast(Client.custom_fields[field_key].astext, String) == str(field_value)
                    )
                except Exception as e:
                    current_app.logger.debug(
                        "JSONB filtering failed for field %s, will use Python filtering: %s", field_key, e
                    )
            if custom_field_conditions:
                query = query.filter(db.or_(*custom_field_conditions))

    # Date range
    if start_date:
        try:
            start_dt = datetime.strptime(start_date, "%Y-%m-%d")
            query = query.filter(TimeEntry.start_time >= start_dt)
        except ValueError:
            pass
    if end_date:
        try:
            end_dt = datetime.strptime(end_date, "%Y-%m-%d").replace(hour=23, minute=59, second=59)
            query = query.filter(TimeEntry.start_time <= end_dt)
        except ValueError:
            pass

    # Paid/billable
    if paid_filter == "true":
        query = query.filter(TimeEntry.paid == True)
    elif paid_filter == "false":
        query = query.filter(TimeEntry.paid == False)

    if billable_filter == "true":
        query = query.filter(TimeEntry.billable == True)
    elif billable_filter == "false":
        query = query.filter(TimeEntry.billable == False)

    # Search in notes/tags
    if search:
        search_pattern = f"%{search}%"
        query = query.filter(or_(TimeEntry.notes.ilike(search_pattern), TimeEntry.tags.ilike(search_pattern)))

    query = query.order_by(desc(TimeEntry.start_time))
    entries = query.all()

    # SQLite (or non-JSONB) custom-field filtering fallback
    if client_custom_field and not is_postgres:
        filtered = []
        for entry in entries:
            if not entry.client:
                continue
            matches = True
            for field_key, field_value in client_custom_field.items():
                if not field_key or not field_value:
                    continue
                client_value = entry.client.custom_fields.get(field_key) if entry.client.custom_fields else None
                if str(client_value) != str(field_value):
                    matches = False
                    break
            if matches:
                filtered.append(entry)
        entries = filtered

    # Build filter context for the PDF report header
    pdf_filters = {}
    if user_id:
        _pdf_user = User.query.get(user_id)
        if _pdf_user:
            pdf_filters["User"] = _pdf_user.username
    if project_id:
        _pdf_project = _project_service.get_by_id(project_id)
        if _pdf_project:
            pdf_filters["Project"] = _pdf_project.name
    if client_id:
        _pdf_client = _client_service.get_by_id(client_id)
        if _pdf_client:
            pdf_filters["Client"] = _pdf_client.name
    if billable_filter:
        pdf_filters["Billable"] = billable_filter
    if paid_filter:
        pdf_filters["Paid"] = paid_filter

    # Generate professional PDF report with ReportLab.
    try:
        from app.utils.time_entries_pdf import build_time_entries_pdf

        pdf_bytes = build_time_entries_pdf(
            entries,
            start_date=start_date or None,
            end_date=end_date or None,
            filters=pdf_filters if pdf_filters else None,
        )
    except Exception as e:
        current_app.logger.warning("Time entries PDF export failed: %s", e, exc_info=True)
        flash(_("PDF export failed: %(error)s", error=str(e)), "error")
        return redirect(url_for("timer.time_entries_overview"))

    # Filename includes optional date range
    start_part = start_date or "all"
    end_part = end_date or "all"
    filename = f"time_entries_{start_part}_to_{end_part}.pdf"

    return send_file(
        io.BytesIO(pdf_bytes),
        mimetype="application/pdf",
        as_attachment=True,
        download_name=filename,
    )


@timer_bp.route("/time-entries/bulk-paid", methods=["POST"])
@login_required
def bulk_mark_paid():
    """Bulk mark time entries as paid or unpaid"""
    from app.utils.db import safe_commit

    entry_ids = request.form.getlist("entry_ids[]")
    paid_status = request.form.get("paid", "").strip().lower()
    invoice_reference = request.form.get("invoice_reference", "").strip()

    if not entry_ids:
        flash(_("No time entries selected"), "warning")
        return redirect(url_for("timer.time_entries_overview"))

    if paid_status not in ("true", "false"):
        flash(_("Invalid paid status"), "error")
        return redirect(url_for("timer.time_entries_overview"))

    is_paid = paid_status == "true"

    # Load entries
    entry_ids_int = [int(eid) for eid in entry_ids if eid.isdigit()]
    if not entry_ids_int:
        flash(_("Invalid entry IDs"), "error")
        return redirect(url_for("timer.time_entries_overview"))

    entries = TimeEntry.query.filter(TimeEntry.id.in_(entry_ids_int)).all()

    if not entries:
        flash(_("No time entries found"), "error")
        return redirect(url_for("timer.time_entries_overview"))

    # Permission check
    can_view_all = current_user.is_admin or current_user.has_permission("view_all_time_entries")
    updated_count = 0
    skipped_count = 0

    for entry in entries:
        # Check permissions
        if not can_view_all and entry.user_id != current_user.id:
            skipped_count += 1
            continue

        # Skip active timers
        if entry.is_active:
            skipped_count += 1
            continue

        # Update paid status with invoice reference if provided
        if is_paid and invoice_reference:
            entry.set_paid(is_paid, invoice_number=invoice_reference)
        else:
            entry.set_paid(is_paid)
        updated_count += 1

        # Log activity
        Activity.log(
            user_id=current_user.id,
            action="updated",
            entity_type="time_entry",
            entity_id=entry.id,
            entity_name=f"Time entry #{entry.id}",
            description=f"Marked time entry as {'paid' if is_paid else 'unpaid'}",
            extra_data={"paid": is_paid, "project_id": entry.project_id, "client_id": entry.client_id},
            ip_address=request.remote_addr,
            user_agent=request.headers.get("User-Agent"),
        )

    if updated_count > 0:
        if not safe_commit("bulk_mark_paid", {"count": updated_count, "paid": is_paid}):
            flash(_("Could not update time entries due to a database error. Please check server logs."), "error")
            return redirect(url_for("timer.time_entries_overview"))

        flash(
            _(
                "Successfully marked %(count)d time entry/entries as %(status)s",
                count=updated_count,
                status=_("paid") if is_paid else _("unpaid"),
            ),
            "success",
        )

    if skipped_count > 0:
        flash(_("Skipped %(count)d time entry/entries (no permission or active timer)", count=skipped_count), "warning")

    # Track event
    track_event(current_user.id, "time_entries.bulk_mark_paid", {"count": updated_count, "paid": is_paid})

    # Preserve filters in redirect
    redirect_url = url_for("timer.time_entries_overview")
    filters = {}
    for key in ["user_id", "project_id", "client_id", "start_date", "end_date", "paid", "billable", "search", "page"]:
        value = request.form.get(key) or request.args.get(key)
        if value:
            filters[key] = value

    if filters:
        redirect_url += "?" + "&".join(f"{k}={v}" for k, v in filters.items())

    return redirect(redirect_url)
