"""Repo mixin for the ``observed_attachments`` table.

Composed onto :class:`SQLModelRepository` alongside the existing
per-domain mixins. The single public method is an upsert: if the
sha256 isn't there, insert with ``observation_count=1`` and the
caller's anchor metadata; otherwise increment ``observation_count``,
roll forward ``last_seen`` and ``last_seen_attacker_uuid``, dedupe a
new ``extension`` into ``extensions``, and stick the
``mal_hash_match`` verdict if either the row had no verdict or the
caller is upgrading ``False/None`` to ``True``.
"""
from __future__ import annotations

from datetime import datetime, timezone
from typing import Optional

from sqlalchemy import select

from decnet.web.db.models import ObservedAttachment
from decnet.web.db.sqlmodel_repo._helpers import _MixinBase


class ObservedAttachmentsMixin(_MixinBase):
    """Mixin: composed onto ``SQLModelRepository``."""

    async def upsert_observed_attachment(
        self,
        *,
        sha256: str,
        decky_uuid: Optional[str],
        attacker_uuid: Optional[str],
        extension: Optional[str],
        subject: Optional[str],
        mal_hash_match: Optional[bool],
        mal_hash_match_provider: Optional[str],
    ) -> str:
        """Record one observation of *sha256*. Returns the row ``uuid``.

        Verdict semantics:

        * Row has no verdict (``None``) → write whatever the caller has,
          including ``None`` (no-op) or ``False`` (provider checked and
          said clean).
        * Row already has ``False`` → upgrade to ``True`` if the caller
          says so; otherwise leave alone.
        * Row already has ``True`` → never downgrade. A hash a feed
          later forgets is still a hash that feed once flagged.
        """
        sha = sha256.lower()
        ext = extension.lower() if extension else None
        now = datetime.now(timezone.utc)

        async with self._session() as session:
            stmt = select(ObservedAttachment).where(
                ObservedAttachment.sha256 == sha,
            )
            row = (await session.execute(stmt)).scalar_one_or_none()
            if row is None:
                row = ObservedAttachment(
                    sha256=sha,
                    first_seen=now,
                    last_seen=now,
                    observation_count=1,
                    first_seen_decky_uuid=decky_uuid,
                    first_seen_attacker_uuid=attacker_uuid,
                    last_seen_attacker_uuid=attacker_uuid,
                    extensions=[ext] if ext else [],
                    first_subject=subject,
                    mal_hash_match=mal_hash_match,
                    mal_hash_match_provider=(
                        mal_hash_match_provider
                        if mal_hash_match is not None
                        else None
                    ),
                    mal_hash_match_at=(
                        now if mal_hash_match is not None else None
                    ),
                )
                session.add(row)
                await session.commit()
                await session.refresh(row)
                return row.uuid

            row.observation_count = (row.observation_count or 0) + 1
            row.last_seen = now
            if attacker_uuid:
                row.last_seen_attacker_uuid = attacker_uuid
            if ext:
                exts = list(row.extensions or [])
                if ext not in exts:
                    exts.append(ext)
                    row.extensions = exts
            # Verdict: only write if the row had no opinion, or the
            # caller is upgrading to True. Never downgrade True.
            if mal_hash_match is True and row.mal_hash_match is not True:
                row.mal_hash_match = True
                row.mal_hash_match_provider = mal_hash_match_provider
                row.mal_hash_match_at = now
            elif (
                mal_hash_match is not None
                and row.mal_hash_match is None
            ):
                row.mal_hash_match = mal_hash_match
                row.mal_hash_match_provider = mal_hash_match_provider
                row.mal_hash_match_at = now
            session.add(row)
            await session.commit()
            return row.uuid
