"""
API endpoints for managing patch records.
"""
import logging
from typing import Optional, List
from fastapi import APIRouter, HTTPException, Query, Depends
from sqlmodel import select
from datetime import datetime

from app.database import SessionDep
from app.models import PatchRecord, PatchRecordRead, PatchRecordResponse, PatchStatus, PatchType
from app.auth import get_current_user
from app.models import User
from app.utils.patch_utils import find_git_repo_root, _to_repo_relative, rewrite_diff_headers, git_validate_diff, count_hunks
from app.utils.patch_db_utils import calculate_file_hash, save_patch_to_db
from app.utils.audit_logger import log_audit_event
from pydantic import BaseModel
import hashlib
import os
import tempfile
import subprocess

logger = logging.getLogger(__name__)

router = APIRouter()


class CreatePatchRequest(BaseModel):
    """Request model for creating a new patch record"""
    file_path: str
    finding_fingerprint: str
    patch_diff: str
    patch_type: PatchType
    source_data: dict


@router.post("/patches")
async def create_patch(
    request: CreatePatchRequest,
    session: SessionDep,
    user: User = Depends(get_current_user)
):
    """
    Create a new patch record in the database.
    Used for saving scanner patches before applying them.
    """
    try:
        patch_id = save_patch_to_db(
            session=session,
            file_path=request.file_path,
            finding_fingerprint=request.finding_fingerprint,
            patch_diff=request.patch_diff,
            patch_type=request.patch_type,
            source_data=request.source_data,
            user_id=user.id,
        )

        if not patch_id:
            raise HTTPException(status_code=500, detail="Failed to create patch record")

        logger.info(f"Created patch {patch_id} for {request.file_path}")

        return {
            "success": True,
            "patch_id": patch_id,
            "message": "Patch created successfully"
        }

    except Exception as e:
        logger.exception("Failed to create patch")
        raise HTTPException(status_code=500, detail=f"Failed to create patch: {str(e)}")


@router.get("/patches", response_model=PatchRecordResponse)
async def list_patches(
    session: SessionDep,
    user: User = Depends(get_current_user),
    file_path: Optional[str] = Query(None, description="Filter by file path"),
    status: Optional[PatchStatus] = Query(None, description="Filter by status"),
    patch_type: Optional[PatchType] = Query(None, description="Filter by patch type"),
    limit: int = Query(100, ge=1, le=1000, description="Maximum number of results"),
    offset: int = Query(0, ge=0, description="Number of results to skip"),
) -> PatchRecordResponse:
    """
    List patch records with optional filtering.

    Query parameters:
    - file_path: Filter patches for a specific file
    - status: Filter by patch status (pending, applied, rolled_back, stale)
    - patch_type: Filter by patch type (scanner, ai)
    - limit: Maximum number of results (default: 100, max: 1000)
    - offset: Pagination offset (default: 0)
    """
    try:
        # Build query - ALWAYS filter by user_id
        statement = select(PatchRecord).where(PatchRecord.user_id == user.id)

        if file_path:
            statement = statement.where(PatchRecord.file_path == file_path)
        if status:
            statement = statement.where(PatchRecord.status == status)
        if patch_type:
            statement = statement.where(PatchRecord.patch_type == patch_type)

        # Order by created_at descending (newest first)
        statement = statement.order_by(PatchRecord.created_at.desc())

        # Apply pagination
        statement = statement.offset(offset).limit(limit)

        # Execute query
        patches = session.exec(statement).all()

        # Convert to read models
        patch_reads = [PatchRecordRead.model_validate(patch) for patch in patches]

        return PatchRecordResponse(
            success=True,
            data=patch_reads,
            message=f"Found {len(patch_reads)} patches"
        )

    except Exception as e:
        logger.exception("Failed to list patches")
        raise HTTPException(status_code=500, detail=f"Failed to list patches: {str(e)}")


@router.get("/patches/{patch_id}", response_model=PatchRecordRead)
async def get_patch(
    patch_id: str,
    session: SessionDep,
    user: User = Depends(get_current_user)
) -> PatchRecordRead:
    """
    Get a specific patch by ID (user can only access their own patches).
    """
    try:
        patch = session.get(PatchRecord, patch_id)
        if not patch:
            raise HTTPException(status_code=404, detail=f"Patch {patch_id} not found")

        # Verify user owns this patch
        if patch.user_id != user.id:
            raise HTTPException(status_code=404, detail=f"Patch {patch_id} not found")

        return PatchRecordRead.model_validate(patch)

    except HTTPException:
        raise
    except Exception as e:
        logger.exception(f"Failed to get patch {patch_id}")
        raise HTTPException(status_code=500, detail=f"Failed to get patch: {str(e)}")


@router.delete("/patches/{patch_id}")
async def delete_patch(
    patch_id: str,
    session: SessionDep,
    user: User = Depends(get_current_user)
):
    """
    Delete a patch record by ID (user can only delete their own patches).
    """
    try:
        patch = session.get(PatchRecord, patch_id)
        if not patch:
            raise HTTPException(status_code=404, detail=f"Patch {patch_id} not found")

        # Verify user owns this patch
        if patch.user_id != user.id:
            raise HTTPException(status_code=404, detail=f"Patch {patch_id} not found")

        session.delete(patch)
        session.commit()

        logger.info(f"Deleted patch {patch_id}")
        log_audit_event("patch_deleted", user_id=user.id, details={"patch_id": patch_id})
        return {"success": True, "message": f"Patch {patch_id} deleted"}

    except HTTPException:
        raise
    except Exception as e:
        logger.exception(f"Failed to delete patch {patch_id}")
        session.rollback()
        raise HTTPException(status_code=500, detail=f"Failed to delete patch: {str(e)}")


@router.post("/patches/{patch_id}/apply")
async def apply_or_rollback_patch(
    patch_id: str,
    session: SessionDep,
    user: User = Depends(get_current_user),
    reverse: bool = False
):
    """
    Apply or rollback a patch by ID using git apply (user can only modify their own patches).

    Args:
        patch_id: ID of the patch to apply/rollback
        reverse: If True, rollback the patch (git apply -R). If False, apply the patch.

    Updates patch status to 'applied' or 'rolled_back' accordingly.
    """
    operation = "rollback" if reverse else "apply"

    try:
        # Get patch record
        patch = session.get(PatchRecord, patch_id)
        if not patch:
            raise HTTPException(status_code=404, detail=f"Patch {patch_id} not found")

        # Verify user owns this patch
        if patch.user_id != user.id:
            raise HTTPException(status_code=404, detail=f"Patch {patch_id} not found")

        # Check patch status (only for rollback)
        if reverse:
            if patch.status != PatchStatus.APPLIED:
                raise HTTPException(status_code=400, detail="Patch is not in applied state")

        # Find git repo
        repo_root = find_git_repo_root(patch.file_path)
        if not repo_root:
            raise HTTPException(status_code=400, detail="File is not in a Git repository")

        # Normalize patch headers
        rel_path = _to_repo_relative(patch.file_path, repo_root)
        normalized = rewrite_diff_headers(patch.patch_diff, rel_path)

        # Write patch to temp file
        with tempfile.NamedTemporaryFile("w", delete=False, suffix=".patch") as tf:
            tf.write(normalized)
            patch_path = tf.name

        try:
            # Get stats from the patch (without validating or applying)
            stat_proc = subprocess.run(
                ["git", "apply", "--stat", patch_path],
                capture_output=True,
                text=True,
            )
            # Parse numstat output (e.g., "1 file changed, 2 insertions(+), 1 deletion(-)")
            added = 0
            removed = 0
            if stat_proc.returncode == 0 and stat_proc.stdout:
                import re
                match = re.search(r'(\d+) insertion', stat_proc.stdout)
                if match:
                    added = int(match.group(1))
                match = re.search(r'(\d+) deletion', stat_proc.stdout)
                if match:
                    removed = int(match.group(1))

            # Apply or reverse-apply patch
            cmd = ["git", "-C", repo_root, "apply"]
            if reverse:
                cmd.append("-R")
            cmd.append(patch_path)

            proc = subprocess.run(cmd, capture_output=True, text=True)

            if proc.returncode != 0:
                raise HTTPException(
                    status_code=500,
                    detail={
                        "message": f"{operation}_failed",
                        "stderr": proc.stderr,
                        "hint": "The file may have changed. Try manually applying the changes." if not reverse
                               else "The file may have changed since the patch was applied. Try manually reverting the changes."
                    }
                )
        finally:
            try:
                os.unlink(patch_path)
            except Exception:
                pass

        # Update patch status
        if reverse:
            patch.status = PatchStatus.ROLLED_BACK
            patch.applied_at = None
        else:
            patch.status = PatchStatus.APPLIED
            patch.applied_at = datetime.utcnow()

        session.add(patch)
        session.commit()

        action = "patch_rolled_back" if reverse else "patch_applied"
        log_audit_event(action, user_id=user.id, details={"patch_id": patch_id, "file_path": patch.file_path})
        logger.info(f"{operation.capitalize()}ed patch {patch_id} {'from' if reverse else 'to'} {patch.file_path}")

        # Read updated content
        try:
            with open(patch.file_path, "r", encoding="utf-8", errors="replace") as f:
                updated_content = f.read()
        except Exception as e:
            raise HTTPException(status_code=500, detail=f"Failed to read updated file: {str(e)}")

        return {
            "success": True,
            "message": f"Patch {operation}ed successfully",
            "patch_id": patch_id,
            "updated_content": updated_content,
            "stats": {
                "added": added or 0,
                "removed": removed or 0,
                "hunks": count_hunks(normalized)
            }
        }

    except HTTPException:
        raise
    except Exception as e:
        logger.exception(f"Failed to {operation} patch {patch_id}")
        session.rollback()
        raise HTTPException(status_code=500, detail=f"Failed to {operation} patch: {str(e)}")


@router.post("/patches/{patch_id}/rollback")
async def rollback_patch(
    patch_id: str,
    session: SessionDep,
    user: User = Depends(get_current_user)
):
    """
    Rollback an applied patch using git apply -R (user can only rollback their own patches).
    Convenience endpoint that calls apply with reverse=True.
    """
    return await apply_or_rollback_patch(patch_id, session, user, reverse=True)


@router.delete("/patches/bulk")
async def delete_all_patches(
    session: SessionDep,
    user: User = Depends(get_current_user),
    confirm: bool = Query(False, description="Must be true to confirm deletion")
):
    """Delete all patches for the current user (for testing/cleanup)"""
    if not confirm:
        raise HTTPException(
            status_code=400,
            detail="Must set confirm=true to delete all patches"
        )

    try:
        statement = select(PatchRecord).where(PatchRecord.user_id == user.id)
        patches = session.exec(statement).all()
        count = len(patches)

        for patch in patches:
            session.delete(patch)

        session.commit()
        logger.info(f"Deleted {count} patches for user {user.id}")
        log_audit_event("delete_all_patches", user_id=user.id, details={"deleted_count": count})

        return {
            "success": True,
            "deleted_count": count,
            "message": f"Deleted {count} patch records"
        }
    except Exception as e:
        logger.exception("Failed to delete patches")
        session.rollback()
        raise HTTPException(status_code=500, detail=f"Failed to delete patches: {str(e)}")
