import csv
import io
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Response
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from sqlalchemy import asc, cast, desc, or_, String

from app.database import get_db
from app.models.user import User, UserRole
from app.models.audit_log import AuditLog
from app.routers.auth import get_current_user
from pydantic import BaseModel
from datetime import datetime

router = APIRouter(
    prefix="/audit",
    tags=["audit"],
    responses={404: {"description": "Not found"}},
)

class AuditLogResponse(BaseModel):
    id: int
    user_id: Optional[int]
    username: Optional[str]
    event_type: str
    event_description: str
    ip_address: Optional[str]
    resource_type: Optional[str]
    resource_id: Optional[str]
    old_value: Optional[str] = None
    new_value: Optional[str] = None
    timestamp: datetime

    class Config:
        # pydantic v2 name (v1's `orm_mode` still works but warns on import)
        from_attributes = True

# Whitelist of sortable columns → SQL column. Anything else falls back to
# timestamp, so the client can't inject an arbitrary order_by.
_SORT_COLUMNS = {
    "timestamp": AuditLog.timestamp,
    "event_type": AuditLog.event_type,
    "resource_type": AuditLog.resource_type,
    "resource_id": AuditLog.resource_id,
    "user_id": AuditLog.user_id,
    "ip_address": AuditLog.ip_address,
}


@router.get("/logs", response_model=List[AuditLogResponse])
async def get_audit_logs(
    response: Response,
    skip: int = 0,
    limit: int = Query(100, le=1000, description="Max rows per page (cap 1000)"),
    user_id: Optional[int] = None,
    resource_type: Optional[str] = None,
    resource_id: Optional[str] = None,
    search: Optional[str] = Query(None, description="Free-text over description, event, resource, IP, user"),
    sort: str = Query("timestamp", description="Sort column"),
    order: str = Query("desc", description="asc | desc"),
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """
    Get audit logs. Sets `X-Total-Count` (matched rows before pagination) so
    the UI can render page controls.

    Admins see everything. Editors can scope the query to a single
    resource (e.g. resource_type='vulnerability', resource_id='42')
    so the vuln detail page can render its own status-change history
    without giving non-admins the full firehose.
    """
    is_admin = current_user.role == UserRole.ADMIN
    is_scoped_resource = resource_type and resource_id
    if not is_admin and not is_scoped_resource:
        raise HTTPException(
            status_code=403,
            detail="Not authorized to view audit logs without a resource scope",
        )

    query = db.query(AuditLog)

    if user_id:
        query = query.filter(AuditLog.user_id == user_id)
    if resource_type:
        query = query.filter(AuditLog.resource_type == resource_type)
    if resource_id:
        query = query.filter(AuditLog.resource_id == resource_id)
    if search and search.strip():
        term = f"%{search.strip()}%"
        # username lives on the related User; outerjoin so system/auto rows
        # (user_id NULL) still match on the other columns.
        query = query.outerjoin(User, AuditLog.user_id == User.id).filter(or_(
            AuditLog.event_description.ilike(term),
            cast(AuditLog.event_type, String).ilike(term),
            AuditLog.resource_type.ilike(term),
            AuditLog.resource_id.ilike(term),
            AuditLog.ip_address.ilike(term),
            User.username.ilike(term),
        ))

    total = query.count()
    response.headers["X-Total-Count"] = str(total)

    col = _SORT_COLUMNS.get(sort, AuditLog.timestamp)
    direction = asc if order == "asc" else desc
    logs = query.order_by(direction(col)).offset(skip).limit(limit).all()

    response = []
    for log in logs:
        response.append({
            "id": log.id,
            "user_id": log.user_id,
            "username": log.user.username if log.user else "System/Auto",
            "event_type": log.event_type.value if hasattr(log.event_type, 'value') else str(log.event_type),
            "event_description": log.event_description,
            "ip_address": log.ip_address,
            "resource_type": log.resource_type,
            "resource_id": log.resource_id,
            "old_value": log.old_value,
            "new_value": log.new_value,
            "timestamp": log.timestamp
        })

    return response


@router.get("/logs/export")
async def export_audit_logs_csv(
    user_id: Optional[int] = None,
    resource_type: Optional[str] = None,
    resource_id: Optional[str] = None,
    since: Optional[datetime] = Query(None, description="ISO timestamp lower bound"),
    until: Optional[datetime] = Query(None, description="ISO timestamp upper bound"),
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """
    Stream the audit log as CSV — full export, no pagination cap.

    Admin-only by default. Editors can export when they scope to a
    single resource (same rule as GET /logs). Filters mirror the
    JSON endpoint plus optional `since` / `until` for revisionssicher
    range exports (e.g. a quarterly compliance dump).

    Uses StreamingResponse + iter chunking so large exports do not
    hold the whole result set in memory.
    """
    is_admin = current_user.role == UserRole.ADMIN
    is_scoped_resource = resource_type and resource_id
    if not is_admin and not is_scoped_resource:
        raise HTTPException(403, "Not authorized to export without a resource scope")

    query = db.query(AuditLog)
    if user_id:
        query = query.filter(AuditLog.user_id == user_id)
    if resource_type:
        query = query.filter(AuditLog.resource_type == resource_type)
    if resource_id:
        query = query.filter(AuditLog.resource_id == resource_id)
    if since:
        query = query.filter(AuditLog.timestamp >= since)
    if until:
        query = query.filter(AuditLog.timestamp <= until)
    query = query.order_by(desc(AuditLog.timestamp))

    def row_iter():
        buf = io.StringIO()
        writer = csv.writer(buf, quoting=csv.QUOTE_MINIMAL)
        writer.writerow([
            "id", "timestamp", "user_id", "username", "event_type",
            "event_description", "ip_address", "user_agent",
            "resource_type", "resource_id", "old_value", "new_value",
        ])
        yield buf.getvalue()
        buf.seek(0); buf.truncate(0)

        # yield_per keeps memory bounded across multi-million-row exports
        for log in query.yield_per(500):
            writer.writerow([
                log.id,
                log.timestamp.isoformat() if log.timestamp else "",
                log.user_id or "",
                (log.user.username if log.user else "System/Auto"),
                log.event_type.value if hasattr(log.event_type, "value") else str(log.event_type),
                (log.event_description or "")[:1000],
                log.ip_address or "",
                (log.user_agent or "")[:300],
                log.resource_type or "",
                log.resource_id or "",
                (log.old_value or "")[:2000],
                (log.new_value or "")[:2000],
            ])
            yield buf.getvalue()
            buf.seek(0); buf.truncate(0)

    fname = f"audit_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
    return StreamingResponse(
        row_iter(),
        media_type="text/csv; charset=utf-8",
        headers={"Content-Disposition": f'attachment; filename="{fname}"'},
    )
