"""
Owner-scoped cloud metadata reconciliation service — Phase 12.

All functions operate within strict (user_id, connection_id) ownership boundaries.
No function here raises FastAPI HTTPException — domain exceptions only.
No function calls the quota service or alters quotas.used_bytes.

Domain exceptions:
  ConnectionNotFound — connection does not exist or belongs to a different user.
  CloudItemNotFound  — cloud item does not exist for the given owner/connection.
"""
from __future__ import annotations

import uuid
from datetime import datetime, timezone
from typing import Optional, Sequence

from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession

from db.models import CloudConnection, CloudFolderState, CloudItem
from storage.cloud_base import CloudListing, CloudResource


# ── Domain exceptions ─────────────────────────────────────────────────────────

class ConnectionNotFound(ValueError):
    """Connection does not exist or belongs to a different user."""


class CloudItemNotFound(ValueError):
    """Cloud item does not exist for the given owner/connection."""


# ── Connection resolution ─────────────────────────────────────────────────────

async def resolve_owned_connection(
    session: AsyncSession,
    *,
    connection_id,
    user_id,
) -> CloudConnection:
    """Return the CloudConnection owned by user_id or raise ConnectionNotFound.

    Accepts UUID objects or string UUIDs for both parameters.
    Never returns a connection belonging to another user.
    """
    conn_uuid = connection_id if isinstance(connection_id, uuid.UUID) else uuid.UUID(str(connection_id))
    user_uuid = user_id if isinstance(user_id, uuid.UUID) else uuid.UUID(str(user_id))
    result = await session.execute(
        select(CloudConnection).where(
            CloudConnection.id == conn_uuid,
            CloudConnection.user_id == user_uuid,
        )
    )
    conn = result.scalars().first()
    if conn is None:
        raise ConnectionNotFound(
            f"Connection {connection_id!r} not found for user {user_id!r}"
        )
    return conn


# ── Item listing ──────────────────────────────────────────────────────────────

async def list_cloud_children(
    session: AsyncSession,
    *,
    user_id: str,
    connection_id: str,
    parent_ref: Optional[str],
) -> Sequence[CloudItem]:
    """Return non-deleted cloud items matching (user_id, connection_id, parent_ref).

    parent_ref=None matches items whose parent_ref is NULL (root children where
    parent is not tracked as a ref string).
    """
    uid_v = user_id if isinstance(user_id, uuid.UUID) else uuid.UUID(str(user_id))
    cid_v = connection_id if isinstance(connection_id, uuid.UUID) else uuid.UUID(str(connection_id))
    stmt = select(CloudItem).where(
        CloudItem.user_id == uid_v,
        CloudItem.connection_id == cid_v,
        CloudItem.deleted_at.is_(None),
    )
    if parent_ref is None:
        stmt = stmt.where(CloudItem.parent_ref.is_(None))
    else:
        stmt = stmt.where(CloudItem.parent_ref == parent_ref)

    result = await session.execute(stmt)
    return result.scalars().all()


# ── Item upsert ───────────────────────────────────────────────────────────────

async def upsert_cloud_item(
    session: AsyncSession,
    *,
    user_id: str,
    resource: CloudResource,
) -> CloudItem:
    """Insert or update a CloudItem for the given normalized resource.

    The DocuVault CloudItem UUID is preserved across provider rename/move:
    if a row already exists for (connection_id, provider_item_id), its id
    is retained and metadata fields are updated in place.

    user_id is always set from the caller — never from the resource alone —
    to enforce the owner boundary.
    """
    conn_uuid2 = resource.connection_id if isinstance(resource.connection_id, uuid.UUID) else uuid.UUID(str(resource.connection_id))
    result = await session.execute(
        select(CloudItem).where(
            CloudItem.connection_id == conn_uuid2,
            CloudItem.provider_item_id == resource.provider_item_id,
        )
    )
    existing = result.scalars().first()

    now = datetime.now(timezone.utc)

    if existing is not None:
        # Update metadata, preserve UUID (stable identity across rename/move)
        existing.name = resource.name
        existing.kind = resource.kind
        existing.parent_ref = resource.parent_ref
        existing.content_type = resource.content_type
        existing.provider_size = resource.size
        existing.etag = resource.etag
        existing.modified_at = resource.modified_at
        existing.last_seen_at = now
        existing.deleted_at = None  # un-delete if previously soft-deleted
        existing.updated_at = now
        await session.flush()
        return existing
    else:
        user_uuid_ins = user_id if isinstance(user_id, uuid.UUID) else uuid.UUID(str(user_id))
        conn_uuid_ins = resource.connection_id if isinstance(resource.connection_id, uuid.UUID) else uuid.UUID(str(resource.connection_id))
        item = CloudItem(
            id=uuid.uuid4(),
            user_id=user_uuid_ins,
            connection_id=conn_uuid_ins,
            provider_item_id=resource.provider_item_id,
            parent_ref=resource.parent_ref,
            name=resource.name,
            kind=resource.kind,
            content_type=resource.content_type,
            provider_size=resource.size,
            etag=resource.etag,
            modified_at=resource.modified_at,
            last_seen_at=now,
            analysis_status="pending",
            semantic_index_status="none",
        )
        session.add(item)
        await session.flush()
        return item


# ── Listing reconciliation ────────────────────────────────────────────────────

async def reconcile_cloud_listing(
    session: AsyncSession,
    *,
    user_id: str,
    connection_id: str,
    parent_ref: Optional[str],
    listing: CloudListing,
) -> None:
    """Reconcile a provider listing against durable cloud_items rows.

    For each item in listing.items: upsert metadata.

    Soft-deletion of missing children is ONLY performed when:
      - listing.complete is True (authoritative, full listing)

    Incomplete or failed listings (complete=False) must NEVER mark retained
    rows deleted — provider bytes remain the source of truth.

    This function never calls the quota service (D-18).
    """
    seen_provider_ids: set[str] = set()

    for resource in listing.items:
        await upsert_cloud_item(session, user_id=user_id, resource=resource)
        seen_provider_ids.add(resource.provider_item_id)

    if listing.complete:
        # Soft-delete items not present in the complete listing
        uid_v2 = user_id if isinstance(user_id, uuid.UUID) else uuid.UUID(str(user_id))
        cid_v2 = connection_id if isinstance(connection_id, uuid.UUID) else uuid.UUID(str(connection_id))
        stmt = select(CloudItem).where(
            CloudItem.user_id == uid_v2,
            CloudItem.connection_id == cid_v2,
            CloudItem.deleted_at.is_(None),
        )
        if parent_ref is None:
            stmt = stmt.where(CloudItem.parent_ref.is_(None))
        else:
            stmt = stmt.where(CloudItem.parent_ref == parent_ref)

        result = await session.execute(stmt)
        existing_items = result.scalars().all()

        now = datetime.now(timezone.utc)
        for item in existing_items:
            if item.provider_item_id not in seen_provider_ids:
                item.deleted_at = now
                item.updated_at = now

        await session.flush()


# ── Folder state helpers ──────────────────────────────────────────────────────

async def get_or_create_folder_state(
    session: AsyncSession,
    *,
    user_id: str,
    connection_id: str,
    parent_ref: str,
) -> CloudFolderState:
    """Return the CloudFolderState for (connection_id, parent_ref), creating if absent.

    Idempotent: repeated calls with the same arguments return the same row.
    parent_ref='' represents the connection root.
    """
    cid_fs = connection_id if isinstance(connection_id, uuid.UUID) else uuid.UUID(str(connection_id))
    uid_fs = user_id if isinstance(user_id, uuid.UUID) else uuid.UUID(str(user_id))
    result = await session.execute(
        select(CloudFolderState).where(
            CloudFolderState.user_id == uid_fs,
            CloudFolderState.connection_id == cid_fs,
            CloudFolderState.parent_ref == parent_ref,
        )
    )
    existing = result.scalars().first()
    if existing is not None:
        return existing

    fs = CloudFolderState(
        id=uuid.uuid4(),
        user_id=uid_fs,
        connection_id=cid_fs,
        parent_ref=parent_ref,
        refresh_state="fresh",
    )
    session.add(fs)
    await session.flush()
    return fs


async def update_folder_state(
    session: AsyncSession,
    *,
    user_id: str,
    connection_id: str,
    parent_ref: str,
    refresh_state: str,
    last_refreshed_at: Optional[datetime] = None,
    error_code: Optional[str] = None,
    error_message: Optional[str] = None,
) -> CloudFolderState:
    """Update the refresh state for a folder.

    Transitions: refreshing → fresh (success) or warning (failure).
    On success: set last_refreshed_at.
    On failure: retain last_refreshed_at, set error_code/message with controlled values.

    error_code and error_message must be controlled service values — never raw
    provider exception text.
    """
    fs = await get_or_create_folder_state(
        session, user_id=user_id, connection_id=connection_id, parent_ref=parent_ref
    )
    now = datetime.now(timezone.utc)
    fs.refresh_state = refresh_state
    fs.updated_at = now

    if last_refreshed_at is not None:
        fs.last_refreshed_at = last_refreshed_at
    elif refresh_state == "fresh":
        fs.last_refreshed_at = now

    if refresh_state == "fresh":
        fs.error_code = None
        fs.error_message = None
    elif error_code is not None:
        fs.error_code = error_code
        fs.error_message = error_message

    await session.flush()
    return fs
