"""Application logging service for audit, usage, and error events."""

import asyncio
import json
import logging
import traceback as tb_module
from datetime import datetime, timedelta, timezone

from sqlalchemy import delete, func, select, text

from scribe.config import Config
from scribe.models import async_session
from scribe.models.app_log import AppLog

logger = logging.getLogger(__name__)

_retention_task: asyncio.Task | None = None


async def log_audit(
    action: str,
    user_id: int | None = None,
    username: str | None = None,
    ip_address: str | None = None,
    details: dict | None = None,
) -> None:
    async with async_session() as session:
        log = AppLog(
            category="audit",
            user_id=user_id,
            username=username,
            action=action,
            ip_address=ip_address,
            details=json.dumps(details) if details else None,
        )
        session.add(log)
        await session.commit()


async def log_usage(
    user_id: int | None = None,
    username: str | None = None,
    endpoint: str | None = None,
    method: str | None = None,
    status_code: int | None = None,
    duration_ms: float | None = None,
) -> None:
    async with async_session() as session:
        log = AppLog(
            category="usage",
            user_id=user_id,
            username=username,
            endpoint=endpoint,
            method=method,
            status_code=status_code,
            duration_ms=duration_ms,
        )
        session.add(log)
        await session.commit()


async def log_error(
    user_id: int | None = None,
    username: str | None = None,
    endpoint: str | None = None,
    method: str | None = None,
    error_type: str | None = None,
    error_message: str | None = None,
    traceback: str | None = None,
) -> None:
    details = {}
    if error_type:
        details["error_type"] = error_type
    if error_message:
        details["error_message"] = error_message
    if traceback:
        details["traceback"] = traceback

    async with async_session() as session:
        log = AppLog(
            category="error",
            user_id=user_id,
            username=username,
            endpoint=endpoint,
            method=method,
            details=json.dumps(details) if details else None,
        )
        session.add(log)
        await session.commit()


def parse_filter_datetime(value: str | None, *, end_of_day: bool = False) -> datetime | None:
    """An ISO date/datetime from a query string as an AWARE UTC datetime.

    The admin log filters arrive as raw `request.args` strings and used to be
    compared straight against `AppLog.created_at`. asyncpg binds a str as
    VARCHAR and Postgres has no `timestamptz >= text` operator, so supplying
    either date filter raised — the same defect as #1727 in notifications, in a
    second place (#2254).

    Returns None for anything unparseable: a malformed filter should narrow
    nothing rather than 500 the log viewer.

    `end_of_day` matters for the upper bound. A bare "2026-07-30" parses to
    midnight, so `created_at <= that` would exclude the whole of the day the
    user asked for — the one day they most likely wanted. With this flag a
    date-only value is pushed to the last microsecond of that day; a value that
    already carries a time is left exactly as given.
    """
    raw = (value or "").strip()
    if not raw:
        return None
    try:
        parsed = datetime.fromisoformat(raw)
    except ValueError:
        return None
    if end_of_day and len(raw) == 10:   # date-only, no time component
        parsed = parsed.replace(hour=23, minute=59, second=59, microsecond=999999)
    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=timezone.utc)
    return parsed


async def get_logs(
    category: str | None = None,
    user_id: int | None = None,
    search: str | None = None,
    date_from: str | None = None,
    date_to: str | None = None,
    limit: int = 50,
    offset: int = 0,
) -> tuple[list[dict], int]:
    async with async_session() as session:
        query = select(AppLog)
        count_query = select(func.count(AppLog.id))

        if category:
            query = query.where(AppLog.category == category)
            count_query = count_query.where(AppLog.category == category)
        if user_id is not None:
            query = query.where(AppLog.user_id == user_id)
            count_query = count_query.where(AppLog.user_id == user_id)
        if search:
            pattern = f"%{search}%"
            search_filter = (
                AppLog.action.ilike(pattern)
                | AppLog.endpoint.ilike(pattern)
                | AppLog.username.ilike(pattern)
                | AppLog.details.ilike(pattern)
            )
            query = query.where(search_filter)
            count_query = count_query.where(search_filter)
        start = parse_filter_datetime(date_from)
        end = parse_filter_datetime(date_to, end_of_day=True)
        if start:
            query = query.where(AppLog.created_at >= start)
            count_query = count_query.where(AppLog.created_at >= start)
        if end:
            query = query.where(AppLog.created_at <= end)
            count_query = count_query.where(AppLog.created_at <= end)

        total = (await session.execute(count_query)).scalar() or 0

        query = query.order_by(AppLog.created_at.desc()).limit(limit).offset(offset)
        result = await session.execute(query)
        logs = [row.to_dict() for row in result.scalars().all()]

        return logs, total


async def get_log_stats() -> dict:
    async with async_session() as session:
        result = await session.execute(
            text(
                "SELECT category, COUNT(*) as count FROM app_logs GROUP BY category"
            )
        )
        stats = {row.category: row.count for row in result}
        return {
            "audit": stats.get("audit", 0),
            "usage": stats.get("usage", 0),
            "error": stats.get("error", 0),
            "total": sum(stats.values()),
        }


async def delete_old_logs(retention_days: int) -> int:
    cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days)
    async with async_session() as session:
        result = await session.execute(
            delete(AppLog).where(AppLog.created_at < cutoff)
        )
        await session.commit()
        return result.rowcount


async def _retention_tick() -> None:
    deleted = await delete_old_logs(Config.LOG_RETENTION_DAYS)
    if deleted:
        logger.info("Log retention: deleted %d old log entries", deleted)


def start_log_retention_loop() -> None:
    global _retention_task
    if _retention_task is None or _retention_task.done():
        from scribe.services.background import start_periodic
        _retention_task = start_periodic(3600, _retention_tick, label="log_retention")  # hourly
