"""Document service: retrieval and version management."""

from __future__ import annotations

import logging
import uuid

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from app.models.document import Document, DocumentItem, ItemVersion

logger = logging.getLogger(__name__)


async def get_document_with_items(slug: str, db: AsyncSession) -> Document | None:
    """Load a document with all its items and their versions, eagerly.

    Parameters
    ----------
    slug:
        Unique slug of the document.
    db:
        Async database session.

    Returns
    -------
    Document | None
        The document with items and versions loaded, or None if not found.
    """
    result = await db.execute(
        select(Document)
        .options(
            selectinload(Document.items).selectinload(DocumentItem.versions)
        )
        .where(Document.slug == slug)
    )
    return result.scalar_one_or_none()


async def apply_version(
    item_id: uuid.UUID,
    version_id: uuid.UUID,
    db: AsyncSession,
) -> DocumentItem:
    """Apply an accepted version to a document item.

    This replaces the item's current_text with the version's proposed_text
    and marks the version as 'accepted'.

    Parameters
    ----------
    item_id:
        UUID of the DocumentItem to update.
    version_id:
        UUID of the ItemVersion to apply.
    db:
        Async database session.

    Returns
    -------
    DocumentItem
        The updated document item.

    Raises
    ------
    ValueError
        If the item or version is not found, or the version does not
        belong to the item.
    """
    # Load item
    item_result = await db.execute(
        select(DocumentItem).where(DocumentItem.id == item_id)
    )
    item = item_result.scalar_one_or_none()
    if item is None:
        raise ValueError(f"Element de document introuvable : {item_id}")

    # Load version
    version_result = await db.execute(
        select(ItemVersion).where(ItemVersion.id == version_id)
    )
    version = version_result.scalar_one_or_none()
    if version is None:
        raise ValueError(f"Version introuvable : {version_id}")

    if version.item_id != item.id:
        raise ValueError(
            f"La version {version_id} n'appartient pas a l'element {item_id}"
        )

    # Apply the version
    item.current_text = version.proposed_text
    version.status = "accepted"

    # Mark all other pending/voting versions for this item as rejected
    other_versions_result = await db.execute(
        select(ItemVersion).where(
            ItemVersion.item_id == item_id,
            ItemVersion.id != version_id,
            ItemVersion.status.in_(["proposed", "voting"]),
        )
    )
    for other_version in other_versions_result.scalars():
        other_version.status = "rejected"

    await db.commit()
    await db.refresh(item)

    return item


async def reject_version(
    item_id: uuid.UUID,
    version_id: uuid.UUID,
    db: AsyncSession,
) -> ItemVersion:
    """Mark a version as rejected.

    Parameters
    ----------
    item_id:
        UUID of the DocumentItem the version belongs to.
    version_id:
        UUID of the ItemVersion to reject.
    db:
        Async database session.

    Returns
    -------
    ItemVersion
        The rejected version.

    Raises
    ------
    ValueError
        If the item or version is not found, or the version does not
        belong to the item.
    """
    # Load item to verify existence
    item_result = await db.execute(
        select(DocumentItem).where(DocumentItem.id == item_id)
    )
    item = item_result.scalar_one_or_none()
    if item is None:
        raise ValueError(f"Element de document introuvable : {item_id}")

    # Load version
    version_result = await db.execute(
        select(ItemVersion).where(ItemVersion.id == version_id)
    )
    version = version_result.scalar_one_or_none()
    if version is None:
        raise ValueError(f"Version introuvable : {version_id}")

    if version.item_id != item.id:
        raise ValueError(
            f"La version {version_id} n'appartient pas a l'element {item_id}"
        )

    version.status = "rejected"
    await db.commit()
    await db.refresh(version)

    return version


async def get_item_with_versions(
    item_id: uuid.UUID,
    db: AsyncSession,
) -> DocumentItem | None:
    """Eager-load a document item with all its versions.

    Parameters
    ----------
    item_id:
        UUID of the DocumentItem.
    db:
        Async database session.

    Returns
    -------
    DocumentItem | None
        The item with versions loaded, or None if not found.
    """
    result = await db.execute(
        select(DocumentItem)
        .options(selectinload(DocumentItem.versions))
        .where(DocumentItem.id == item_id)
    )
    return result.scalar_one_or_none()


async def reorder_items(
    document_id: uuid.UUID,
    items_order: list[tuple[uuid.UUID, int]],
    db: AsyncSession,
) -> list[DocumentItem]:
    """Update sort_order for multiple items in a document.

    Parameters
    ----------
    document_id:
        UUID of the document.
    items_order:
        List of (item_id, sort_order) tuples.
    db:
        Async database session.

    Returns
    -------
    list[DocumentItem]
        The updated items, ordered by sort_order.

    Raises
    ------
    ValueError
        If any item is not found or does not belong to the document.
    """
    for item_id, sort_order in items_order:
        result = await db.execute(
            select(DocumentItem).where(
                DocumentItem.id == item_id,
                DocumentItem.document_id == document_id,
            )
        )
        item = result.scalar_one_or_none()
        if item is None:
            raise ValueError(
                f"Element {item_id} introuvable dans le document {document_id}"
            )
        item.sort_order = sort_order

    await db.commit()

    # Return all items in new order
    result = await db.execute(
        select(DocumentItem)
        .where(DocumentItem.document_id == document_id)
        .order_by(DocumentItem.sort_order)
    )
    return list(result.scalars().all())


def serialize_document_to_text(doc: Document) -> str:
    """Serialize a document and its items to a plain-text representation.

    The items must be eagerly loaded on the document before calling this.

    Parameters
    ----------
    doc:
        Document with items loaded.

    Returns
    -------
    str
        Plain-text serialization suitable for hashing and archival.
    """
    lines: list[str] = []
    lines.append(f"# {doc.title}")
    lines.append(f"Version: {doc.version}")
    lines.append(f"Type: {doc.doc_type}")
    lines.append(f"Statut: {doc.status}")
    if doc.description:
        lines.append(f"Description: {doc.description}")
    lines.append("")

    # Sort items by sort_order
    sorted_items = sorted(doc.items, key=lambda i: i.sort_order)
    for item in sorted_items:
        header = f"## {item.position}"
        if item.title:
            header += f" - {item.title}"
        header += f" [{item.item_type}]"
        lines.append(header)
        lines.append(item.current_text)
        lines.append("")

    return "\n".join(lines)


async def archive_document(slug: str, db: AsyncSession):
    """Serialize a document to text and archive it to the sanctuary.

    Parameters
    ----------
    slug:
        Slug of the document to archive.
    db:
        Async database session.

    Returns
    -------
    SanctuaryEntry
        The newly created sanctuary entry.

    Raises
    ------
    ValueError
        If the document is not found.
    """
    from app.services import sanctuary_service

    doc = await get_document_with_items(slug, db)
    if doc is None:
        raise ValueError(f"Document introuvable : {slug}")

    content = serialize_document_to_text(doc)

    entry = await sanctuary_service.archive_to_sanctuary(
        entry_type="document",
        reference_id=doc.id,
        content=content,
        title=f"{doc.title} v{doc.version}",
        db=db,
    )

    # Update document with sanctuary references
    if entry.ipfs_cid:
        doc.ipfs_cid = entry.ipfs_cid
    if entry.chain_tx_hash:
        doc.chain_anchor = entry.chain_tx_hash

    await db.commit()
    await db.refresh(doc)

    return entry
