"""Refresh-token ledger for the EnCoach API.

Access tokens stay short-lived (1 hour) and stateless — they are verified
with the JWT signature alone. Refresh tokens, on the other hand, must be
revocable so compromised devices can be signed out without rotating the
server secret. Every refresh token corresponds to one row here, keyed by a
UUID (``jti``) that the client echoes back on ``/api/auth/refresh``.

Rotation policy: ``/api/auth/refresh`` consumes the presented row (revokes
it) and issues a brand-new row, so a stolen refresh token only works once.
Replay attempts after rotation will be rejected because the replayed ``jti``
is no longer ``active``.
"""

from __future__ import annotations

import logging

from odoo import api, fields, models

_logger = logging.getLogger(__name__)


class EncoachJwtToken(models.Model):
    _name = "encoach.jwt.token"
    _description = "JWT Refresh Token Ledger"
    _order = "issued_at desc"
    _rec_name = "jti"

    jti = fields.Char(
        string="Token ID",
        required=True,
        index=True,
        help="UUID used as the JWT's `jti` claim; serves as the primary handle.",
    )
    user_id = fields.Many2one(
        "res.users",
        string="User",
        required=True,
        ondelete="cascade",
        index=True,
    )
    issued_at = fields.Datetime(required=True, default=fields.Datetime.now)
    expires_at = fields.Datetime(required=True)
    last_used_at = fields.Datetime()
    revoked = fields.Boolean(default=False, index=True)
    user_agent = fields.Char(help="User-Agent header of the issuing client, for audit.")
    remote_ip = fields.Char(help="Remote IP of the issuing client, for audit.")

    @api.model
    def _auto_init(self):
        res = super()._auto_init()
        cr = self.env.cr
        for name, ddl in (
            (
                "encoach_jwt_token_jti_unique",
                "CREATE UNIQUE INDEX IF NOT EXISTS encoach_jwt_token_jti_unique "
                "ON encoach_jwt_token (jti)",
            ),
            (
                "encoach_jwt_token_user_revoked_idx",
                "CREATE INDEX IF NOT EXISTS encoach_jwt_token_user_revoked_idx "
                "ON encoach_jwt_token (user_id, revoked, expires_at)",
            ),
        ):
            try:
                cr.execute(ddl)
            except Exception:
                _logger.warning("could not create index %s", name, exc_info=True)
        return res

    @api.model
    def revoke_by_jti(self, jti: str) -> bool:
        """Soft-revoke every row matching ``jti``.

        Returns ``True`` iff at least one row was flipped. Called from
        ``/api/auth/refresh`` (to consume the rotated token) and
        ``/api/logout`` (to end the session).
        """
        if not jti:
            return False
        rows = self.sudo().search([("jti", "=", jti), ("revoked", "=", False)])
        if not rows:
            return False
        rows.write({"revoked": True})
        return True

    @api.model
    def purge_expired(self, batch_size: int = 500) -> int:
        """Remove rows that expired more than a day ago.

        Wired to the daily cleanup cron in :file:`data/cron.xml`; manual
        invocations (e.g. migrations) should pass a larger ``batch_size``
        to avoid multi-pass overhead.
        """
        from datetime import datetime, timedelta
        cutoff = datetime.utcnow() - timedelta(days=1)
        rows = self.sudo().search(
            [("expires_at", "<", cutoff)],
            limit=batch_size,
        )
        n = len(rows)
        if n:
            rows.unlink()
        return n
