"""User, administrator, settings, audit and rate-limit persistence."""

from __future__ import annotations

import json
from datetime import datetime, timedelta
from typing import Any

from sqlalchemy import delete, func, select
from sqlalchemy.exc import IntegrityError

from app.database.base import utcnow
from app.database.models import (
    Admin,
    AppSetting,
    AuditLog,
    OperationLog,
    OperationStatus,
    RateLimitEvent,
    User,
)
from app.database.repositories.base import BaseRepository


class UserRepository(BaseRepository):
    async def get_by_telegram_id(self, telegram_id: int) -> User | None:
        async with self.db.session() as session:
            return await session.scalar(select(User).where(User.telegram_id == telegram_id))

    async def get_or_create(
        self,
        telegram_id: int,
        *,
        username: str | None = None,
        display_name: str | None = None,
    ) -> User:
        now = utcnow()
        async with self.db.session() as session:
            user = await session.scalar(select(User).where(User.telegram_id == telegram_id))
            if user is None:
                user = User(
                    telegram_id=telegram_id,
                    username=username,
                    display_name=display_name,
                    first_seen=now,
                    last_active=now,
                )
                session.add(user)
                try:
                    await session.flush()
                except IntegrityError:
                    await session.rollback()
                    user = await session.scalar(
                        select(User).where(User.telegram_id == telegram_id)
                    )
                    if user is None:
                        raise
            else:
                user.last_active = now
                if username:
                    user.username = username
                if display_name:
                    user.display_name = display_name
            return user

    async def record_request(self, telegram_id: int) -> None:
        async with self.db.session() as session:
            user = await session.scalar(select(User).where(User.telegram_id == telegram_id))
            if user is None:
                return
            now = utcnow()
            user.request_count += 1
            user.last_request_at = now
            user.last_active = now

    async def set_blocked(self, telegram_id: int, blocked: bool, reason: str | None) -> bool:
        async with self.db.session() as session:
            user = await session.scalar(select(User).where(User.telegram_id == telegram_id))
            if user is None:
                return False
            user.is_blocked = blocked
            user.blocked_reason = reason
            return True

    async def list_users(self, limit: int = 20, offset: int = 0) -> list[User]:
        async with self.db.session() as session:
            rows = await session.scalars(
                select(User).order_by(User.last_active.desc()).limit(limit).offset(offset)
            )
            return list(rows)

    async def count(self) -> int:
        async with self.db.session() as session:
            return int(await session.scalar(select(func.count(User.id))) or 0)

    async def count_active_since(self, since: datetime) -> int:
        async with self.db.session() as session:
            return int(
                await session.scalar(
                    select(func.count(User.id)).where(User.last_active >= since)
                )
                or 0
            )


class AdminRepository(BaseRepository):
    async def sync_env_admins(self, admin_ids: list[int]) -> None:
        if not admin_ids:
            return
        async with self.db.session() as session:
            # There is exactly one primary administrator: the first entry in
            # ADMIN_IDS. Clear stale primary flags before seeding.
            await session.execute(
                Admin.__table__.update()
                .where(Admin.is_env_admin.is_(True))
                .values(is_primary=False)
            )
            for index, telegram_id in enumerate(admin_ids):
                admin = await session.scalar(
                    select(Admin).where(Admin.telegram_id == telegram_id)
                )
                if admin is None:
                    admin = Admin(
                        telegram_id=telegram_id,
                        title="مدیر اصلی" if index == 0 else "مدیر محیطی",
                        is_primary=index == 0,
                        is_env_admin=True,
                        is_active=True,
                    )
                    session.add(admin)
                else:
                    admin.is_env_admin = True
                    admin.is_active = True
                    admin.is_primary = index == 0
            # Env admins removed from ADMIN_IDS lose panel access, but rows remain
            # for audit history. DB-added admins are untouched.
            await session.execute(
                Admin.__table__.update()
                .where(Admin.is_env_admin.is_(True), Admin.telegram_id.not_in(admin_ids))
                .values(is_active=False, is_primary=False)
            )

    async def is_admin(self, telegram_id: int) -> bool:
        async with self.db.session() as session:
            admin = await session.scalar(
                select(Admin).where(
                    Admin.telegram_id == telegram_id,
                    Admin.is_active.is_(True),
                )
            )
            return admin is not None

    async def get(self, telegram_id: int) -> Admin | None:
        async with self.db.session() as session:
            return await session.scalar(select(Admin).where(Admin.telegram_id == telegram_id))

    async def list_active(self) -> list[Admin]:
        async with self.db.session() as session:
            rows = await session.scalars(
                select(Admin).where(Admin.is_active.is_(True)).order_by(Admin.created_at)
            )
            return list(rows)

    async def add(self, telegram_id: int, title: str | None, added_by: int) -> tuple[Admin, bool]:
        async with self.db.session() as session:
            existing = await session.scalar(select(Admin).where(Admin.telegram_id == telegram_id))
            if existing is not None:
                if existing.is_active:
                    return existing, False
                existing.is_active = True
                existing.title = title or existing.title
                existing.added_by = added_by
                return existing, True
            admin = Admin(
                telegram_id=telegram_id,
                title=title,
                is_primary=False,
                is_env_admin=False,
                is_active=True,
                added_by=added_by,
            )
            session.add(admin)
            await session.flush()
            return admin, True

    async def remove(self, telegram_id: int) -> tuple[bool, str]:
        async with self.db.session() as session:
            admin = await session.scalar(select(Admin).where(Admin.telegram_id == telegram_id))
            if admin is None:
                return False, "مدیر پیدا نشد."
            if admin.is_primary:
                return False, "مدیر اصلی قابل حذف نیست."
            if admin.is_env_admin:
                return False, "این مدیر در ADMIN_IDS تعریف شده و فقط با تغییر env قابل حذف است."
            admin.is_active = False
            return True, "مدیر غیرفعال شد."


class SettingsRepository(BaseRepository):
    async def get(self, key: str, default: str | None = None) -> str | None:
        async with self.db.session() as session:
            setting = await session.get(AppSetting, key)
            return setting.value if setting else default

    async def get_bool(self, key: str, default: bool = False) -> bool:
        value = await self.get(key)
        if value is None:
            return default
        text = value.strip().lower()
        if text in {"1", "true", "yes", "on"}:
            return True
        if text in {"0", "false", "no", "off"}:
            return False
        return default

    async def get_int(self, key: str, default: int = 0) -> int:
        value = await self.get(key)
        try:
            return int(value) if value is not None else default
        except (TypeError, ValueError):
            return default

    async def set(self, key: str, value: str, updated_by: int | None = None) -> None:
        async with self.db.session() as session:
            setting = await session.get(AppSetting, key)
            if setting is None:
                session.add(AppSetting(key=key, value=value, updated_by=updated_by))
            else:
                setting.value = value
                setting.updated_by = updated_by

    async def delete(self, key: str) -> None:
        async with self.db.session() as session:
            setting = await session.get(AppSetting, key)
            if setting:
                await session.delete(setting)

    async def all(self) -> dict[str, str]:
        async with self.db.session() as session:
            rows = (await session.scalars(select(AppSetting))).all()
            return {row.key: row.value for row in rows}


class AuditRepository(BaseRepository):
    async def log(
        self,
        actor_id: int | None,
        action: str,
        *,
        entity_type: str | None = None,
        entity_id: str | int | None = None,
        details: dict[str, Any] | None = None,
        correlation_id: str | None = None,
    ) -> None:
        async with self.db.session() as session:
            session.add(
                AuditLog(
                    actor_id=actor_id,
                    action=action,
                    entity_type=entity_type,
                    entity_id=str(entity_id) if entity_id is not None else None,
                    details=details or {},
                    correlation_id=correlation_id,
                )
            )

    async def list_recent(self, limit: int = 20) -> list[AuditLog]:
        async with self.db.session() as session:
            rows = await session.scalars(
                select(AuditLog).order_by(AuditLog.created_at.desc(), AuditLog.id.desc()).limit(limit)
            )
            return list(rows)


class OperationRepository(BaseRepository):
    async def start(
        self,
        correlation_id: str,
        operation: str,
        *,
        entity_type: str | None = None,
        entity_id: str | int | None = None,
        meta: dict[str, Any] | None = None,
    ) -> int:
        async with self.db.session() as session:
            row = OperationLog(
                correlation_id=correlation_id,
                operation=operation,
                status=OperationStatus.STARTED,
                entity_type=entity_type,
                entity_id=str(entity_id) if entity_id is not None else None,
                meta=meta or {},
            )
            session.add(row)
            await session.flush()
            return row.id

    async def finish(
        self,
        operation_id: int,
        status: OperationStatus,
        *,
        error_message: str | None = None,
        meta: dict[str, Any] | None = None,
    ) -> None:
        async with self.db.session() as session:
            row = await session.get(OperationLog, operation_id)
            if row is None:
                return
            row.status = status
            row.error_message = error_message
            row.finished_at = utcnow()
            if meta:
                row.meta = {**(row.meta or {}), **meta}

    async def recent_failures(self, limit: int = 20) -> list[OperationLog]:
        async with self.db.session() as session:
            rows = await session.scalars(
                select(OperationLog)
                .where(OperationLog.status.in_([OperationStatus.FAILED, OperationStatus.UNKNOWN]))
                .order_by(OperationLog.created_at.desc())
                .limit(limit)
            )
            return list(rows)


class RateLimitRepository(BaseRepository):
    async def count_since(self, telegram_id: int, action: str, since: datetime) -> int:
        async with self.db.session() as session:
            return int(
                await session.scalar(
                    select(func.count(RateLimitEvent.id)).where(
                        RateLimitEvent.telegram_id == telegram_id,
                        RateLimitEvent.action == action,
                        RateLimitEvent.created_at >= since,
                    )
                )
                or 0
            )

    async def last_event_at(self, telegram_id: int, action: str) -> datetime | None:
        async with self.db.session() as session:
            return await session.scalar(
                select(RateLimitEvent.created_at)
                .where(
                    RateLimitEvent.telegram_id == telegram_id,
                    RateLimitEvent.action == action,
                )
                .order_by(RateLimitEvent.created_at.desc())
                .limit(1)
            )

    async def add(self, telegram_id: int, action: str) -> None:
        async with self.db.session() as session:
            session.add(RateLimitEvent(telegram_id=telegram_id, action=action))

    async def prune(self, older_than: timedelta) -> int:
        cutoff = utcnow() - older_than
        async with self.db.session() as session:
            result = await session.execute(
                delete(RateLimitEvent).where(RateLimitEvent.created_at < cutoff)
            )
            return int(result.rowcount or 0)


def json_dumps(value: Any) -> str:
    return json.dumps(value, ensure_ascii=False, separators=(",", ":"))


def json_loads(value: str, default: Any) -> Any:
    try:
        return json.loads(value)
    except (ValueError, TypeError):
        return default

