"""
Document Vault service.

Tenant isolation strategy (F-02 mitigation):
  PostgreSQL RLS requires the `app.tenant` session variable to be set, which is a
  platform-wide concern tracked separately.  Until that is implemented, every query
  in this service EXPLICITLY filters `Document.tenant_id == tenant_id` where
  `tenant_id` is sourced from `current_user.tenant_id` (validated by the JWT
  middleware).  This prevents cross-tenant data access without relying on RLS.
"""

import asyncio
import os
import uuid
from typing import Optional
from uuid import UUID

import boto3
from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession

from src.apps.documents.models.document import Document
from src.apps.site_admin.services.audit_service import AuditService
from src.core.config import settings
from src.core.exceptions import NotFoundError, ValidationError

ALLOWED_MIME_TYPES = {
    "application/pdf",
    "image/jpeg",
    "image/png",
    "image/tiff",
}

MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024  # 20 MB

MIME_TO_EXT = {
    "application/pdf": "pdf",
    "image/jpeg": "jpg",
    "image/png": "png",
    "image/tiff": "tiff",
}


def _ext_from_filename(filename: str) -> str:
    """Extract lowercase extension from filename, without the leading dot."""
    _, ext = os.path.splitext(filename)
    return ext.lstrip(".").lower() if ext else "bin"


def _build_s3_key(tenant_id: UUID, entity_type: str, entity_id: UUID, filename: str) -> str:
    ext = _ext_from_filename(filename)
    unique = uuid.uuid4()
    return f"tenant/{tenant_id}/documents/{entity_type}/{entity_id}/{unique}.{ext}"


def _make_s3_client():
    """Create a boto3 S3 client per-request to avoid credential caching issues."""
    return boto3.client(
        "s3",
        region_name=settings.S3_DOCUMENTS_REGION,
        aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
        aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
    )


def _apply_cdn(url: str) -> str:
    """Replace the S3 bucket origin with the CloudFront distribution URL.

    CloudFront is configured to forward query-string parameters, so S3
    presigned auth (X-Amz-Signature et al.) passes through unchanged.
    Falls back to the raw S3 URL when CLOUDFRONT_DOCUMENTS_URL is not set.
    """
    cdn = getattr(settings, "CLOUDFRONT_DOCUMENTS_URL", "") or ""
    if not cdn:
        return url
    # Strip trailing slash from CDN base
    cdn = cdn.rstrip("/")
    # Replace https://<bucket>.s3.amazonaws.com or https://<bucket>.s3.<region>.amazonaws.com
    import re
    return re.sub(
        r"https://[^/]+\.amazonaws\.com",
        cdn,
        url,
        count=1,
    )


class DocumentService:
    def __init__(self, db: AsyncSession):
        self.db = db

    async def upload_file(
        self,
        tenant_id: UUID,
        entity_type: str,
        entity_id: UUID,
        filename: str,
        mime_type: str,
        file_content: bytes,
        file_size_bytes: int,
        category: Optional[str],
        uploaded_by: UUID,
        current_user,
        request,
    ) -> "Document":
        """Upload a file directly to S3 from the API server (proxy upload).

        Bypasses the browser→S3 presigned POST flow entirely, so no S3 CORS
        policy is required on the bucket.
        """
        import io

        if mime_type not in ALLOWED_MIME_TYPES:
            raise ValidationError(
                message=(
                    f"Unsupported file type '{mime_type}'. "
                    f"Allowed types: PDF, JPEG, PNG, TIFF."
                )
            )

        if file_size_bytes > MAX_FILE_SIZE_BYTES:
            raise ValidationError(message="File size exceeds the 20 MB limit.")

        s3_key = _build_s3_key(tenant_id, entity_type, entity_id, filename)

        s3 = _make_s3_client()
        await asyncio.to_thread(
            s3.put_object,
            Bucket=settings.S3_DOCUMENTS_BUCKET,
            Key=s3_key,
            Body=file_content,
            ContentType=mime_type,
        )

        doc = Document(
            tenant_id=tenant_id,
            entity_type=entity_type,
            entity_id=entity_id,
            s3_key=s3_key,
            filename=filename,
            file_size_bytes=file_size_bytes,
            mime_type=mime_type,
            category=category,
            version=1,
            uploaded_by=uploaded_by,
            upload_confirmed=True,
        )
        self.db.add(doc)
        await self.db.flush()

        await AuditService.log(
            self.db,
            entity_type="document",
            entity_id=doc.id,
            action="document_upload",
            user=current_user,
            request=request,
        )

        return doc

    async def get_upload_url(
        self,
        tenant_id: UUID,
        entity_type: str,
        entity_id: UUID,
        filename: str,
        mime_type: str,
        file_size_bytes: Optional[int],
        category: Optional[str],
        uploaded_by: UUID,
    ) -> dict:
        # Tenant isolation: tenant_id comes from JWT (current_user.tenant_id).
        # 1. Validate mime_type
        if mime_type not in ALLOWED_MIME_TYPES:
            raise ValidationError(
                message=(
                    f"Unsupported file type '{mime_type}'. "
                    f"Allowed types: PDF, JPEG, PNG, TIFF."
                )
            )

        # 2. Validate file size
        if file_size_bytes is not None and file_size_bytes > MAX_FILE_SIZE_BYTES:
            raise ValidationError(
                message="File size exceeds the 20 MB limit."
            )

        # 3. Build S3 key
        s3_key = _build_s3_key(tenant_id, entity_type, entity_id, filename)

        # 4. Create S3 client and generate presigned POST (F-04).
        #    presigned_post enforces content-length-range at the S3 layer, preventing
        #    clients from uploading files larger than MAX_FILE_SIZE_BYTES.
        s3 = _make_s3_client()
        conditions = [
            ["content-length-range", 1, MAX_FILE_SIZE_BYTES],
            {"Content-Type": mime_type},
        ]
        presign_response: dict = await asyncio.to_thread(
            s3.generate_presigned_post,
            settings.S3_DOCUMENTS_BUCKET,
            s3_key,
            Fields={"Content-Type": mime_type},
            Conditions=conditions,
            ExpiresIn=300,  # 5 minutes
        )
        # presign_response = {"url": "...", "fields": {"Content-Type": ..., "key": ..., ...}}

        # 5. Insert pending Document row
        doc = Document(
            tenant_id=tenant_id,
            entity_type=entity_type,
            entity_id=entity_id,
            s3_key=s3_key,
            filename=filename,
            file_size_bytes=file_size_bytes,
            mime_type=mime_type,
            category=category,
            version=1,
            uploaded_by=uploaded_by,
            upload_confirmed=False,
        )
        self.db.add(doc)
        await self.db.flush()  # get doc.id without committing

        return {
            "document_id": str(doc.id),
            "upload_url": presign_response["url"],
            "upload_fields": presign_response["fields"],
        }

    async def confirm_upload(
        self,
        tenant_id: UUID,
        document_id: UUID,
        uploaded_by: UUID,  # F-03: must match the user who initiated the upload
        current_user,
        request,
        file_size_bytes: Optional[int] = None,
    ) -> Document:
        # Tenant isolation: tenant_id == current_user.tenant_id (from JWT).
        # F-03: additionally bind to the originating user via uploaded_by.
        result = await self.db.execute(
            select(Document).where(
                and_(
                    Document.id == document_id,
                    Document.tenant_id == tenant_id,
                    Document.uploaded_by == uploaded_by,  # F-03: user binding
                    Document.upload_confirmed.is_(False),
                    Document.deleted_at.is_(None),
                )
            )
        )
        doc = result.scalar_one_or_none()
        if not doc:
            raise NotFoundError("Pending upload not found or already confirmed.")

        # F-03: validate size cap even when provided at confirm time
        if file_size_bytes is not None and file_size_bytes > MAX_FILE_SIZE_BYTES:
            raise ValidationError(message="File size exceeds the 20 MB limit.")

        doc.upload_confirmed = True
        if file_size_bytes is not None:
            doc.file_size_bytes = file_size_bytes

        await self.db.flush()

        # F-06: audit log
        await AuditService.log(
            self.db,
            entity_type="document",
            entity_id=doc.id,
            action="document_upload",
            user=current_user,
            request=request,
        )

        return doc

    async def get_list(
        self,
        tenant_id: UUID,
        entity_type: str,
        entity_id: UUID,
        page: int = 1,
        page_size: int = 20,
    ) -> tuple[list[Document], int]:
        # Tenant isolation: tenant_id == current_user.tenant_id (from JWT).
        conditions = [
            Document.tenant_id == tenant_id,
            Document.entity_type == entity_type,
            Document.entity_id == entity_id,
            Document.upload_confirmed.is_(True),
            Document.deleted_at.is_(None),
        ]

        count_result = await self.db.execute(
            select(func.count(Document.id)).where(and_(*conditions))
        )
        total = count_result.scalar_one()

        offset = (page - 1) * page_size
        data_result = await self.db.execute(
            select(Document)
            .where(and_(*conditions))
            .order_by(Document.created_at.desc())
            .offset(offset)
            .limit(page_size)
        )
        docs = data_result.scalars().all()
        return list(docs), total

    async def get_download_url(
        self,
        tenant_id: UUID,
        document_id: UUID,
        current_user,
        request,
    ) -> str:
        # Tenant isolation: tenant_id == current_user.tenant_id (from JWT).
        result = await self.db.execute(
            select(Document).where(
                and_(
                    Document.id == document_id,
                    Document.tenant_id == tenant_id,
                    Document.upload_confirmed.is_(True),
                    Document.deleted_at.is_(None),
                )
            )
        )
        doc = result.scalar_one_or_none()
        if not doc:
            raise NotFoundError("Document not found.")

        s3 = _make_s3_client()
        raw_url: str = await asyncio.to_thread(
            lambda: s3.generate_presigned_url(
                "get_object",
                Params={
                    "Bucket": settings.S3_DOCUMENTS_BUCKET,
                    "Key": doc.s3_key,
                },
                ExpiresIn=900,  # 15 minutes
            )
        )
        presigned_url = _apply_cdn(raw_url)

        # F-06: audit log
        await AuditService.log(
            self.db,
            entity_type="document",
            entity_id=doc.id,
            action="document_download",
            user=current_user,
            request=request,
        )

        return presigned_url

    async def soft_delete(
        self,
        tenant_id: UUID,
        document_id: UUID,
        current_user,
        request,
    ) -> Document:
        # Tenant isolation: tenant_id == current_user.tenant_id (from JWT).
        # F-13: only allow deleting confirmed (fully uploaded) documents.
        result = await self.db.execute(
            select(Document).where(
                and_(
                    Document.id == document_id,
                    Document.tenant_id == tenant_id,
                    Document.upload_confirmed.is_(True),  # F-13: guard
                    Document.deleted_at.is_(None),
                )
            )
        )
        doc = result.scalar_one_or_none()
        if not doc:
            raise NotFoundError("Document not found.")

        doc.soft_delete()
        await self.db.flush()

        # F-06: audit log
        await AuditService.log(
            self.db,
            entity_type="document",
            entity_id=doc.id,
            action="document_delete",
            user=current_user,
            request=request,
        )

        return doc
