# #region AssistantHistory [C:2] [TYPE Module] [SEMANTICS assistant, history, audit, persistence, conversation]
# @BRIEF Conversation history, audit trail, and confirmation persistence helpers for the assistant API.
# @LAYER API
# @RELATION DEPENDS_ON -> [AssistantSchemas]
# @INVARIANT  Failed persistence attempts always rollback before returning.

from __future__ import annotations

from datetime import UTC, datetime, timedelta
from typing import Any
import uuid

from sqlalchemy.orm import Session

from src.core.logger import logger
from src.models.assistant import (
    AssistantAuditRecord,
    AssistantConfirmationRecord,
    AssistantMessageRecord,
)

from ._schemas import (
    ASSISTANT_ARCHIVE_AFTER_DAYS,
    ASSISTANT_AUDIT,
    ASSISTANT_MESSAGE_TTL_DAYS,
    CONVERSATIONS,
    ConfirmationRecord,
)

logger = logger


# #region _append_history [C:2] [TYPE Function]
# @BRIEF Append conversation message to in-memory history buffer.
# @DATA_CONTRACT  Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]
# @RELATION BINDS_TO -> [CONVERSATIONS]
# @SIDE_EFFECT  Mutates in-memory CONVERSATIONS store for user conversation history.
# @PRE  user_id and conversation_id identify target conversation bucket.
# @POST  Message entry is appended to CONVERSATIONS key list.
# @INVARIANT  every appended entry includes generated message_id and created_at timestamp.
def _append_history(
    user_id: str,
    conversation_id: str,
    role: str,
    text: str,
    state: str | None = None,
    task_id: str | None = None,
    confirmation_id: str | None = None,
):
    key = (user_id, conversation_id)
    if key not in CONVERSATIONS:
        CONVERSATIONS[key] = []
    CONVERSATIONS[key].append(
        {
            "message_id": str(uuid.uuid4()),
            "conversation_id": conversation_id,
            "role": role,
            "text": text,
            "state": state,
            "task_id": task_id,
            "confirmation_id": confirmation_id,
            "created_at": datetime.now(UTC),
        }
    )


# #endregion _append_history


# #region _persist_message [C:2] [TYPE Function]
# @BRIEF Persist assistant/user message record to database.
# @DATA_CONTRACT  Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None]
# @RELATION DEPENDS_ON -> [AssistantMessageRecord]
# @SIDE_EFFECT  Writes AssistantMessageRecord rows and commits or rollbacks the DB session.
# @PRE  db session is writable and message payload is serializable.
# @POST  Message row is committed or persistence failure is logged.
# @INVARIANT  failed persistence attempts always rollback before returning.
def _persist_message(
    db: Session,
    user_id: str,
    conversation_id: str,
    role: str,
    text: str,
    state: str | None = None,
    task_id: str | None = None,
    confirmation_id: str | None = None,
    metadata: dict[str, Any] | None = None,
):
    try:
        row = AssistantMessageRecord(
            id=str(uuid.uuid4()),
            user_id=user_id,
            conversation_id=conversation_id,
            role=role,
            text=text,
            state=state,
            task_id=task_id,
            confirmation_id=confirmation_id,
            payload=metadata,
        )
        db.add(row)
        db.commit()
    except Exception as exc:
        db.rollback()
        logger.warning(f"[assistant.message][persist_failed] {exc}")


# #endregion _persist_message


# #region _audit [C:2] [TYPE Function]
# @BRIEF Append in-memory audit record for assistant decision trace.
# @DATA_CONTRACT  Input[user_id,payload:Dict[str,Any]] -> Output[None]
# @RELATION BINDS_TO -> [ASSISTANT_AUDIT]
# @SIDE_EFFECT  Mutates in-memory ASSISTANT_AUDIT store and emits structured log event.
# @PRE  payload describes decision/outcome fields.
# @POST  ASSISTANT_AUDIT list for user contains new timestamped entry.
# @INVARIANT  persisted in-memory audit entry always contains created_at in ISO format.
def _audit(user_id: str, payload: dict[str, Any]):
    if user_id not in ASSISTANT_AUDIT:
        ASSISTANT_AUDIT[user_id] = []
    ASSISTANT_AUDIT[user_id].append(
        {**payload, "created_at": datetime.now(UTC).isoformat()}
    )
    logger.info(f"[assistant.audit] {payload}")


# #endregion _audit


# #region _persist_audit [C:2] [TYPE Function]
# @BRIEF Persist structured assistant audit payload in database.
# @PRE  db session is writable and payload is JSON-serializable.
# @POST  Audit row is committed or failure is logged with rollback.
def _persist_audit(
    db: Session, user_id: str, payload: dict[str, Any], conversation_id: str | None
):
    try:
        row = AssistantAuditRecord(
            id=str(uuid.uuid4()),
            user_id=user_id,
            conversation_id=conversation_id,
            decision=payload.get("decision"),
            task_id=payload.get("task_id"),
            message=payload.get("message"),
            payload=payload,
        )
        db.add(row)
        db.commit()
    except Exception as exc:
        db.rollback()
        logger.warning(f"[assistant.audit][persist_failed] {exc}")


# #endregion _persist_audit


# #region _persist_confirmation [C:2] [TYPE Function]
# @BRIEF Persist confirmation token record to database.
# @PRE  record contains id/user/intent/dispatch/expiry fields.
# @POST  Confirmation row exists in persistent storage.
def _persist_confirmation(db: Session, record: ConfirmationRecord):
    try:
        row = AssistantConfirmationRecord(
            id=record.id,
            user_id=record.user_id,
            conversation_id=record.conversation_id,
            state=record.state,
            intent=record.intent,
            dispatch=record.dispatch,
            expires_at=record.expires_at,
            created_at=record.created_at,
            consumed_at=None,
        )
        db.merge(row)
        db.commit()
    except Exception as exc:
        db.rollback()
        logger.warning(f"[assistant.confirmation][persist_failed] {exc}")


# #endregion _persist_confirmation


# #region _update_confirmation_state [C:2] [TYPE Function]
# @BRIEF Update persistent confirmation token lifecycle state.
# @PRE  confirmation_id references existing row.
# @POST  State and consumed_at fields are updated when applicable.
def _update_confirmation_state(db: Session, confirmation_id: str, state: str):
    try:
        row = (
            db.query(AssistantConfirmationRecord)
            .filter(AssistantConfirmationRecord.id == confirmation_id)
            .first()
        )
        if not row:
            return
        row.state = state
        if state in {"consumed", "expired", "cancelled"}:
            row.consumed_at = datetime.now(UTC)
        db.commit()
    except Exception as exc:
        db.rollback()
        logger.warning(f"[assistant.confirmation][update_failed] {exc}")


# #endregion _update_confirmation_state


# #region _load_confirmation_from_db [C:2] [TYPE Function]
# @BRIEF Load confirmation token from database into in-memory model.
# @PRE  confirmation_id may or may not exist in storage.
# @POST  Returns ConfirmationRecord when found, otherwise None.
def _load_confirmation_from_db(
    db: Session, confirmation_id: str
) -> ConfirmationRecord | None:
    row = (
        db.query(AssistantConfirmationRecord)
        .filter(AssistantConfirmationRecord.id == confirmation_id)
        .first()
    )
    if not row:
        return None
    return ConfirmationRecord(
        id=row.id,
        user_id=row.user_id,
        conversation_id=row.conversation_id,
        intent=row.intent or {},
        dispatch=row.dispatch or {},
        expires_at=row.expires_at,
        state=row.state,
        created_at=row.created_at,
    )


# #endregion _load_confirmation_from_db


# #region _ensure_conversation [C:2] [TYPE Function]
# @BRIEF Resolve active conversation id in memory or create a new one.
# @PRE  user_id identifies current actor.
# @POST  Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.
def _ensure_conversation(user_id: str, conversation_id: str | None) -> str:
    if conversation_id:
        from ._schemas import USER_ACTIVE_CONVERSATION
        USER_ACTIVE_CONVERSATION[user_id] = conversation_id
        return conversation_id

    from ._schemas import USER_ACTIVE_CONVERSATION
    active = USER_ACTIVE_CONVERSATION.get(user_id)
    if active:
        return active

    new_id = str(uuid.uuid4())
    USER_ACTIVE_CONVERSATION[user_id] = new_id
    return new_id


# #endregion _ensure_conversation


# #region _resolve_or_create_conversation [C:2] [TYPE Function]
# @BRIEF Resolve active conversation using explicit id, memory cache, or persisted history.
# @PRE  user_id and db session are available.
# @POST  Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.
def _resolve_or_create_conversation(
    user_id: str, conversation_id: str | None, db: Session
) -> str:
    from ._schemas import USER_ACTIVE_CONVERSATION

    if conversation_id:
        USER_ACTIVE_CONVERSATION[user_id] = conversation_id
        return conversation_id

    active = USER_ACTIVE_CONVERSATION.get(user_id)
    if active:
        return active

    from sqlalchemy import desc

    last_message = (
        db.query(AssistantMessageRecord)
        .filter(AssistantMessageRecord.user_id == user_id)
        .order_by(desc(AssistantMessageRecord.created_at))
        .first()
    )
    if last_message:
        USER_ACTIVE_CONVERSATION[user_id] = last_message.conversation_id
        return last_message.conversation_id

    new_id = str(uuid.uuid4())
    USER_ACTIVE_CONVERSATION[user_id] = new_id
    return new_id


# #endregion _resolve_or_create_conversation


# #region _cleanup_history_ttl [C:2] [TYPE Function]
# @BRIEF Enforce assistant message retention window by deleting expired rows and in-memory records.
# @PRE  db session is available and user_id references current actor scope.
# @POST  Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.
def _cleanup_history_ttl(db: Session, user_id: str):
    cutoff = datetime.now() - timedelta(days=ASSISTANT_MESSAGE_TTL_DAYS)
    try:
        query = db.query(AssistantMessageRecord).filter(
            AssistantMessageRecord.user_id == user_id,
            AssistantMessageRecord.created_at < cutoff,
        )
        if hasattr(query, "delete"):
            query.delete(synchronize_session=False)
            db.commit()
    except Exception as exc:
        db.rollback()
        logger.warning(
            f"[assistant.history][ttl_cleanup_failed] user={user_id} error={exc}"
        )

    stale_keys: list[tuple[str, str]] = []
    for key, items in CONVERSATIONS.items():
        if key[0] != user_id:
            continue
        kept = []
        for item in items:
            created_at = item.get("created_at")
            if isinstance(created_at, datetime):
                # DB stores naive datetimes; in-memory items may be aware (from datetime.now(UTC))
                ref = created_at.replace(tzinfo=None) if created_at.tzinfo else created_at
                if ref < cutoff:
                    continue
            kept.append(item)
        if kept:
            CONVERSATIONS[key] = kept
        else:
            stale_keys.append(key)
    for key in stale_keys:
        CONVERSATIONS.pop(key, None)


# #endregion _cleanup_history_ttl


# #region _is_conversation_archived [C:2] [TYPE Function]
# @BRIEF Determine archived state for a conversation based on last update timestamp.
# @PRE  updated_at can be null for empty conversations.
# @POST  Returns True when conversation inactivity exceeds archive threshold.
def _is_conversation_archived(updated_at: datetime | None) -> bool:
    if not updated_at:
        return False
    cutoff = datetime.now() - timedelta(days=ASSISTANT_ARCHIVE_AFTER_DAYS)
    # Ensure both operands are naive (DB stores naive datetimes)
    ref = updated_at.replace(tzinfo=None) if updated_at.tzinfo else updated_at
    return ref < cutoff


# #endregion _is_conversation_archived


# #region _coerce_query_bool [C:2] [TYPE Function]
# @BRIEF Normalize bool-like query values for compatibility in direct handler invocations/tests.
# @PRE  value may be bool, string, or FastAPI Query metadata object.
# @POST  Returns deterministic boolean flag.
def _coerce_query_bool(value: Any) -> bool:
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        return value.strip().lower() in {"1", "true", "yes", "on"}
    return False


# #endregion _coerce_query_bool


# #endregion AssistantHistory
