import json
import os
import tempfile
from pathlib import Path

from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render
from django.views.decorators.http import require_http_methods

DEFAULT_TIMEOUT_SECONDS = 15 * 60
MIN_TIMEOUT_SECONDS = 1
MAX_TIMEOUT_SECONDS = 8 * 60 * 60


def _is_admin(user):
    return bool(
        user.is_authenticated
        and user.is_active
        and user.is_staff
        and user.is_superuser
    )


def _config_path():
    return Path(settings.BASE_DIR) / "session_timeout.json"


def _read_timeout_seconds():
    """Read the configured timeout, supporting legacy and current JSON formats."""
    try:
        with _config_path().open("r", encoding="utf-8") as file:
            data = json.load(file)

        if not isinstance(data, dict):
            return DEFAULT_TIMEOUT_SECONDS

        if "seconds_total" in data:
            total_seconds = int(data["seconds_total"])
        elif any(key in data for key in ("hours", "minutes", "seconds")):
            hours = int(data.get("hours", 0))
            minutes = int(data.get("minutes", 0))
            seconds = int(data.get("seconds", 0))
            if hours < 0 or not 0 <= minutes <= 59 or not 0 <= seconds <= 59:
                return DEFAULT_TIMEOUT_SECONDS
            total_seconds = hours * 3600 + minutes * 60 + seconds
        else:
            # Backward compatibility with {"minutes": 15}.
            total_seconds = int(data.get("minutes", 15)) * 60

        if MIN_TIMEOUT_SECONDS <= total_seconds <= MAX_TIMEOUT_SECONDS:
            return total_seconds
    except (OSError, ValueError, TypeError, json.JSONDecodeError):
        pass

    return DEFAULT_TIMEOUT_SECONDS


def _timeout_parts(total_seconds):
    """Return values expected by the Hours / Minutes / Seconds template."""
    total_seconds = max(MIN_TIMEOUT_SECONDS, min(int(total_seconds), MAX_TIMEOUT_SECONDS))
    hours, remainder = divmod(total_seconds, 3600)
    minutes, seconds = divmod(remainder, 60)
    return {"hours": hours, "minutes": minutes, "seconds": seconds}


def _save_timeout_seconds(total_seconds):
    """Atomically save the timeout without leaving a partially written JSON file."""
    target = _config_path()
    temp_path = None

    try:
        with tempfile.NamedTemporaryFile(
            mode="w",
            encoding="utf-8",
            dir=str(target.parent),
            prefix=".session_timeout_",
            suffix=".tmp",
            delete=False,
        ) as file:
            temp_path = file.name
            json.dump({"seconds_total": total_seconds}, file, indent=2)
            file.write("\n")
            file.flush()
            os.fsync(file.fileno())

        os.replace(temp_path, target)
        return True

    except OSError:
        if temp_path:
            try:
                os.unlink(temp_path)
            except OSError:
                pass
        return False


def _render_settings(request, *, hours=None, minutes=None, seconds=None):
    if hours is None or minutes is None or seconds is None:
        parts = _timeout_parts(_read_timeout_seconds())
    else:
        parts = {"hours": hours, "minutes": minutes, "seconds": seconds}

    parts.update({
        "min_seconds": MIN_TIMEOUT_SECONDS,
        "max_hours": MAX_TIMEOUT_SECONDS // 3600,
    })
    return render(request, "accounts/session_settings.html", parts)


@login_required
@require_http_methods(["GET", "POST"])
def session_settings(request):
    if not _is_admin(request.user):
        messages.error(
            request,
            "Access denied. Only authorised administrators can change session security settings.",
        )
        return redirect("dashboard")

    if request.method == "POST":
        raw_hours = request.POST.get("hours", "0").strip()
        raw_minutes = request.POST.get("minutes", "0").strip()
        raw_seconds = request.POST.get("seconds", "0").strip()

        if not all(value.isdigit() for value in (raw_hours, raw_minutes, raw_seconds)):
            messages.error(request, "Enter whole numbers for hours, minutes and seconds.")
            return _render_settings(request)

        hours = int(raw_hours)
        minutes = int(raw_minutes)
        seconds = int(raw_seconds)

        if hours > 8 or not 0 <= minutes <= 59 or not 0 <= seconds <= 59:
            messages.error(
                request,
                "Use hours from 0 to 8, and minutes and seconds from 0 to 59.",
            )
            return _render_settings(
                request, hours=hours, minutes=minutes, seconds=seconds
            )

        total_seconds = hours * 3600 + minutes * 60 + seconds
        if not MIN_TIMEOUT_SECONDS <= total_seconds <= MAX_TIMEOUT_SECONDS:
            messages.error(
                request,
                "Session timeout must be at least 1 second and no more than 8 hours.",
            )
            return _render_settings(
                request, hours=hours, minutes=minutes, seconds=seconds
            )

        if _save_timeout_seconds(total_seconds):
            messages.success(
                request,
                f"Session timeout saved as {hours} hours, {minutes} minutes and {seconds} seconds.",
            )
        else:
            messages.error(
                request,
                "Could not save the setting. Check file permissions and try again.",
            )

        return redirect("session_settings")

    return _render_settings(request)
