import asyncio
import csv
import io
import os
import uuid as _uuid
from datetime import datetime, timezone
from uuid import UUID, uuid4

from fastapi import APIRouter, Depends, File, Query, Request, UploadFile
from fastapi.responses import StreamingResponse
from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession

from src.apps.news.models.news_post import NewsPost
from src.apps.news.schemas.requests import CreateNewsRequest, UpdateNewsRequest
from src.apps.news.schemas.responses import NewsPostResponse
from src.core.config import settings
from src.core.constants import UserRole
from src.core.dependencies import get_current_user, require_min_role
from src.core.exceptions import NotFoundError, ValidationError
from src.core.schemas.response import paginated, success
from src.database.session import get_db

router = APIRouter(prefix="/news", tags=["News"])


def _resolve_status(requested_status: str, publish_date: datetime | None) -> tuple[str, datetime | None]:
    """
    Return (status, published_at) based on the requested status and publish_date.
    - publish: future publish_date → scheduled, no published_at
    - publish: past/today publish_date or no date → published, published_at = now
    - draft → draft, no change to published_at
    """
    if requested_status == "published":
        now = datetime.now(timezone.utc)
        if publish_date and publish_date.replace(tzinfo=timezone.utc if publish_date.tzinfo is None else publish_date.tzinfo) > now:
            return "scheduled", None
        return "published", now
    return requested_status, None


@router.get("", response_model=dict)
async def list_news(
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
    search: str = Query(None),
    status: str = Query(None),
    category: str = Query(None),
    current_user=Depends(require_min_role(UserRole.VIEW_ONLY)),
    db: AsyncSession = Depends(get_db),
):
    skip = (page - 1) * page_size
    conditions = [
        NewsPost.tenant_id == current_user.tenant_id,
        NewsPost.deleted_at.is_(None),
    ]
    if status:
        conditions.append(NewsPost.status == status)
    if category:
        conditions.append(NewsPost.category == category)
    if search:
        term = f"%{search.lower()}%"
        conditions.append(func.lower(NewsPost.title).like(term))

    where_clause = and_(*conditions)

    count_result = await db.execute(select(func.count(NewsPost.id)).where(where_clause))
    total = count_result.scalar_one()

    result = await db.execute(
        select(NewsPost)
        .where(where_clause)
        .order_by(NewsPost.published_at.desc().nulls_last(), NewsPost.created_at.desc())
        .offset(skip)
        .limit(page_size)
    )
    posts = result.scalars().all()
    return paginated(
        items=[NewsPostResponse.model_validate(p).model_dump() for p in posts],
        total=total,
        page=page,
        page_size=page_size,
    )


@router.get("/export", response_class=StreamingResponse)
async def export_news_csv(
    search: str = Query(None),
    status: str = Query(None),
    category: str = Query(None),
    current_user=Depends(require_min_role(UserRole.STAFF)),
    db: AsyncSession = Depends(get_db),
):
    conditions = [
        NewsPost.tenant_id == current_user.tenant_id,
        NewsPost.deleted_at.is_(None),
    ]
    if status:
        conditions.append(NewsPost.status == status)
    if category:
        conditions.append(NewsPost.category == category)
    if search:
        term = f"%{search.lower()}%"
        conditions.append(func.lower(NewsPost.title).like(term))

    result = await db.execute(
        select(NewsPost)
        .where(and_(*conditions))
        .order_by(NewsPost.published_at.desc().nulls_last(), NewsPost.created_at.desc())
    )
    posts = result.scalars().all()

    output = io.StringIO()
    writer = csv.writer(output)
    writer.writerow(["ID", "Title", "Category", "Excerpt", "Status", "Published At", "Views", "Created At"])
    for p in posts:
        writer.writerow([
            str(p.id),
            p.title,
            p.category or "",
            (p.excerpt or "").replace("\n", " "),
            p.status,
            p.published_at.isoformat() if p.published_at else "",
            p.views_count,
            p.created_at.isoformat(),
        ])

    output.seek(0)
    return StreamingResponse(
        iter([output.getvalue()]),
        media_type="text/csv",
        headers={"Content-Disposition": "attachment; filename=news-export.csv"},
    )


@router.post("", response_model=dict, status_code=201)
async def create_news(
    body: CreateNewsRequest,
    current_user=Depends(require_min_role(UserRole.STAFF)),
    db: AsyncSession = Depends(get_db),
):
    import re as _re
    data = body.model_dump(exclude_none=True)
    if not data.get("slug"):
        raw = _re.sub(r"[^a-z0-9]+", "-", data["title"].lower()).strip("-")
        data["slug"] = raw or "post"
    resolved_status, published_at = _resolve_status(data.get("status", "draft"), data.get("publish_date"))
    data["status"] = resolved_status
    if published_at is not None:
        data["published_at"] = published_at
    post = NewsPost(
        tenant_id=current_user.tenant_id,
        author_id=current_user.id,
        **data,
    )

    db.add(post)
    await db.flush()
    await db.refresh(post)
    return success(
        data=NewsPostResponse.model_validate(post).model_dump(),
        message="News post created",
    )


@router.post("/{post_id}/image", response_model=dict)
async def upload_news_image(
    post_id: UUID,
    file: UploadFile = File(...),
    current_user=Depends(require_min_role(UserRole.STAFF)),
    db: AsyncSession = Depends(get_db),
):
    """Upload a cover image for a news post. Stores in S3 and saves the key on the post."""
    import boto3
    import botocore.exceptions

    ALLOWED_MIME = {"image/jpeg", "image/png", "image/webp", "image/gif"}
    mime = file.content_type or "application/octet-stream"
    if mime not in ALLOWED_MIME:
        raise ValidationError("Only JPEG, PNG, WebP, and GIF images are allowed.")

    result = await db.execute(
        select(NewsPost).where(
            and_(
                NewsPost.id == post_id,
                NewsPost.tenant_id == current_user.tenant_id,
                NewsPost.deleted_at.is_(None),
            )
        )
    )
    post = result.scalar_one_or_none()
    if not post:
        raise NotFoundError("News post not found")

    content = await file.read()
    if len(content) > 10 * 1024 * 1024:
        raise ValidationError("Image must be under 10 MB.")

    ext = (file.filename or "image").rsplit(".", 1)[-1].lower() or "jpg"
    key = f"tenant/{current_user.tenant_id}/news/{post_id}/{_uuid.uuid4()}.{ext}"

    if not settings.AWS_ACCESS_KEY_ID or not settings.S3_DOCUMENTS_BUCKET:
        raise ValidationError("S3 storage is not configured.")

    s3 = boto3.client(
        "s3",
        aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
        aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
        region_name=settings.S3_DOCUMENTS_REGION or "us-east-1",
    )
    try:
        await asyncio.to_thread(
            lambda: s3.put_object(
                Bucket=settings.S3_DOCUMENTS_BUCKET,
                Key=key,
                Body=content,
                ContentType=mime,
            )
        )
    except botocore.exceptions.ClientError as exc:
        raise ValidationError(f"Image upload failed: {exc}") from exc

    post.cover_image_s3_key = key
    await db.flush()
    await db.refresh(post)
    return success(
        data={"cover_image_s3_key": key},
        message="Image uploaded successfully",
    )


@router.get("/{post_id}", response_model=dict)
async def get_news(
    post_id: UUID,
    current_user=Depends(require_min_role(UserRole.VIEW_ONLY)),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(NewsPost).where(
            and_(
                NewsPost.id == post_id,
                NewsPost.tenant_id == current_user.tenant_id,
                NewsPost.deleted_at.is_(None),
            )
        )
    )
    post = result.scalar_one_or_none()
    if not post:
        raise NotFoundError("News post not found")
    return success(data=NewsPostResponse.model_validate(post).model_dump())


@router.put("/{post_id}", response_model=dict)
async def update_news(
    post_id: UUID,
    body: UpdateNewsRequest,
    current_user=Depends(require_min_role(UserRole.STAFF)),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(NewsPost).where(
            and_(
                NewsPost.id == post_id,
                NewsPost.tenant_id == current_user.tenant_id,
                NewsPost.deleted_at.is_(None),
            )
        )
    )
    post = result.scalar_one_or_none()
    if not post:
        raise NotFoundError("News post not found")

    update_data = body.model_dump(exclude_none=True)

    # Re-evaluate status if status or publish_date is changing
    new_status = update_data.get("status", post.status)
    new_publish_date = update_data.get("publish_date", post.publish_date)
    resolved_status, published_at = _resolve_status(new_status, new_publish_date)
    update_data["status"] = resolved_status
    if published_at is not None:
        update_data["published_at"] = published_at
    elif new_status == "draft" and "status" in update_data:
        update_data["published_at"] = None

    for field, value in update_data.items():
        if hasattr(post, field):
            setattr(post, field, value)

    await db.flush()
    await db.refresh(post)
    return success(
        data=NewsPostResponse.model_validate(post).model_dump(),
        message="News post updated",
    )


@router.delete("/{post_id}", status_code=204)
async def delete_news(
    post_id: UUID,
    current_user=Depends(require_min_role(UserRole.MANAGER)),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(NewsPost).where(
            and_(
                NewsPost.id == post_id,
                NewsPost.tenant_id == current_user.tenant_id,
                NewsPost.deleted_at.is_(None),
            )
        )
    )
    post = result.scalar_one_or_none()
    if not post:
        raise NotFoundError("News post not found")
    post.soft_delete()
    await db.flush()


@router.patch("/{post_id}/publish", response_model=dict)
async def toggle_publish(
    post_id: UUID,
    request: Request,
    current_user=Depends(require_min_role(UserRole.STAFF)),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(NewsPost).where(
            and_(
                NewsPost.id == post_id,
                NewsPost.tenant_id == current_user.tenant_id,
                NewsPost.deleted_at.is_(None),
            )
        )
    )
    post = result.scalar_one_or_none()
    if not post:
        raise NotFoundError("News post not found")

    if post.status == "published":
        post.status = "draft"
        post.published_at = None
    else:
        post.status = "published"
        post.published_at = datetime.now(timezone.utc)
        if hasattr(request.app.state, "arq_pool") and request.app.state.arq_pool:
            await request.app.state.arq_pool.enqueue_job(
                "send_news_notification",
                str(current_user.tenant_id),
                str(post.id),
            )

    await db.flush()
    await db.refresh(post)
    action = "published" if post.status == "published" else "unpublished"
    return success(
        data=NewsPostResponse.model_validate(post).model_dump(),
        message=f"News post {action}",
    )
