"""
FastAPI Application Entry Point
"""
import asyncio
import errno
import os
import sys
import json

# Add parent directory to sys.path to allow 'from app...' imports when run as a script
_current_dir = os.path.dirname(os.path.abspath(__file__))
_parent_dir = os.path.dirname(_current_dir)
if _parent_dir not in sys.path:
    sys.path.insert(0, _parent_dir)

from fastapi import FastAPI, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.exceptions import HTTPException

# Use SafeJSONResponse so all dict responses are cleaned of numpy NaN / non-JSON-serializable values
from app.core.json_encoder import SafeJSONResponse
import fastapi.routing as _fastapi_routing
_fastapi_routing.JSONResponse = SafeJSONResponse
from starlette.datastructures import FormData
from starlette.formparsers import MultiPartParser, FormParser
from typing import Any

# ── Override Request.form() to handle JSON-to-Form and raise multipart per-part size limit
_MAX_UPLOAD_SIZE = 100 * 1024 * 1024  # 100 MB

# Make the patch idempotent and avoid recursion during reloads
if not hasattr(Request.form, "_is_patched"):
    # Store the true original form ONLY if it hasn't been patched yet
    # This prevents chaining patched versions during hot-reloads
    _true_original_form = Request.form
    
    async def _patched_form(
        self: Request,
        *,
        max_files: int | float = 1000,
        max_fields: int | float = 1000,
        **kwargs: Any,
    ) -> FormData:
        """
        Patched version of Request.form() that:
        1. Transparently handles application/json by converting it to FormData.
        2. Overrides max_part_size for actual multipart requests.
        """
        # 1. Handle JSON-to-Form conversion
        content_type = self.headers.get("content-type", "")
        if "application/json" in content_type:
            try:
                body_json = await self.json()
                if isinstance(body_json, dict):
                    form_items = []
                    for k, v in body_json.items():
                        if v is None:
                            continue
                        # Use json.dumps for complex objects, str for simple ones
                        val = str(v) if not isinstance(v, (dict, list)) else json.dumps(v)
                        form_items.append((str(k), val))
                    return FormData(form_items)
            except Exception:
                pass

        # 2. Call the true original for multipart/form-data with custom max_part_size
        return await _true_original_form(
            self,
            max_files=max_files,
            max_fields=max_fields,
            max_part_size=_MAX_UPLOAD_SIZE,
            **kwargs
        )

    _patched_form._is_patched = True  # type: ignore
    Request.form = _patched_form  # type: ignore[method-assign]

from app.core.config import settings
from app.core.database import engine, Base


# Import all routes (import app.models so action_pipeline tables register with Base before init_database)
import app.models  # noqa: F401
from app.routes import questions, distributions, schedules, dashboards, admin, admin_options, auth, initial_questions, askme, training, download, logs, static_files


from contextlib import asynccontextmanager


def _install_asyncio_client_disconnect_handler() -> None:
    """
    Windows + Proactor: when the client closes the socket first, asyncio may log
    ``ConnectionResetError: [WinError 10054]`` from ``_ProactorBasePipeTransport._call_connection_lost``.
    That is benign; suppress it so logs stay usable. Other exceptions still use the default handler.
    """
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        return

    previous = loop.get_exception_handler()

    def _handler(loop_ref: asyncio.AbstractEventLoop, context: dict) -> None:
        exc = context.get("exception")
        if isinstance(exc, (ConnectionResetError, BrokenPipeError, ConnectionAbortedError)):
            return
        if isinstance(exc, OSError):
            winerr = getattr(exc, "winerror", None)
            if winerr in (10054, 10053):  # forcibly closed / aborted (Windows)
                return
            if exc.errno in (
                errno.ECONNRESET,
                errno.EPIPE,
                errno.ECONNABORTED,
            ):
                return
        if previous is not None:
            previous(loop_ref, context)
        else:
            loop_ref.default_exception_handler(context)

    # Only Windows Proactor commonly spams this on normal client disconnects.
    if sys.platform == "win32":
        loop.set_exception_handler(_handler)


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Lifespan event handler for application startup and shutdown"""
    # --- Startup Logic ---
    from app.core.database import init_database, SessionLocal
    from app.models.user import User
    from datetime import datetime
    
    init_database()
    _install_asyncio_client_disconnect_handler()

    try:
        from app.services.document_service import (
            FILE_EXTRACT_DISK_ROOT,
            FILE_IMAGE_DISK_ROOT,
            FILE_SYS_METADATA_DB,
            FILE_VECTORS_DB_DIR,
        )
        from app.utils.bm25_checkpoint import check_startup_and_maybe_schedule
        from app.utils.page_snapshot_cleanup import cleanup_extracted_images_once

        check_startup_and_maybe_schedule(
            chroma_path=FILE_VECTORS_DB_DIR,
            openai_api_key=settings.OPENAI_API_KEY,
        )
        cleanup_extracted_images_once(
            FILE_SYS_METADATA_DB,
            extracted_folders=[FILE_EXTRACT_DISK_ROOT],
            preview_folder=FILE_IMAGE_DISK_ROOT,
        )
    except Exception as e:
        print(f"BM25 startup checkpoint warning: {e}")

    # Check if default admin user exists
    db = SessionLocal()
    try:
        # Check if any user exists
        existing_user = db.query(User).first()
        if not existing_user:
            print("No users found. Creating default admin user 'askme360admin'...")
            current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            
            new_admin = User(
                name="askme360 Admin",
                email="askme360admin",
                contact="0000000000",
                password="adminRock@#4321",
                passkey="admin_passkey",
                end_date="2099-12-31",
                status="Active",
                user_token="admin_token",
                created_at=current_time,
                updated_at=current_time
            )
            
            db.add(new_admin)
            db.commit()
            print("Default admin user 'askme360admin' created successfully")
        else:
            print("Users already exist. Skipping default admin creation.")
    except Exception as e:
        print(f"Error checking/creating default user: {e}")
    finally:
        db.close()

    yield
    
    # --- Shutdown Logic ---
    from app.core.database import engine, checkpoint_wal
    try:
        engine.dispose()  # Close all pool connections
        checkpoint_wal("TRUNCATE")  # Flush WAL into .db and truncate WAL file
    except Exception as e:
        print(f"Shutdown checkpoint warning: {e}")
    # Shutdown dedicated long-running executor for askme (avoids thread leak)
    try:
        from app.routes import askme
        if hasattr(askme, "_LONG_RUNNING_EXECUTOR"):
            askme._LONG_RUNNING_EXECUTOR.shutdown(wait=False)
    except Exception:
        pass

app = FastAPI(
    title=settings.APP_NAME,
    version=settings.APP_VERSION,
    description="askme360 Backend API",
    root_path="/askmeapi",
    lifespan=lifespan,
    # docs_url=None
)

# CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# Custom exception handler for authentication errors
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    """
    Custom exception handler to convert HTTPException to standard API response format.
    Authentication errors (401, 403) return {"success": false, "message": "Invalid user"}
    """
    # Handle authentication/authorization errors
    if exc.status_code in [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN]:
        return JSONResponse(
            status_code=exc.status_code,
            content={
                "success": False,
                "message": "Invalid user"
            }
        )
    
    # For other HTTPExceptions, return standard format
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "success": False,
            "message": exc.detail
        }
    )



@app.get("/", tags=["Health"])
async def root():
    """Health check"""
    return {
        "message": "AskMe360 Backend API",
        "version": settings.APP_VERSION,
        "status": "healthy"
    }


# Include all routers
app.include_router(questions.router, prefix="/api")
app.include_router(logs.router, prefix="/api")
app.include_router(distributions.router, prefix="/api")
app.include_router(schedules.router, prefix="/api")
app.include_router(dashboards.router, prefix="/api")
app.include_router(admin.router, prefix="/api")
app.include_router(admin_options.router, prefix="/api")
app.include_router(auth.router, prefix="/api")
app.include_router(initial_questions.router, prefix="/api")
app.include_router(askme.router, prefix="/api")
app.include_router(download.router, prefix="/api")
app.include_router(training.router, prefix="/api")
app.include_router(static_files.router, prefix="/api")


if __name__ == "__main__":
    import uvicorn
    import multiprocessing
    import sys
    import os

    # Add the root directory to sys.path so we can import separate_scheduler
    # root_dir is the parent directory of 'app'
    root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    if root_dir not in sys.path:
        sys.path.insert(0, root_dir)

    try:
        from separate_scheduler import main as scheduler_main
        # Start the scheduler in a separate process
        scheduler_process = multiprocessing.Process(target=scheduler_main, daemon=True)
        scheduler_process.start()
        print(f"--- Started separate_scheduler in process {scheduler_process.pid} ---")
    except ImportError:
        print("--- Warning: separate_scheduler.py not found. Starting only Uvicorn. ---")
    except Exception as e:
        print(f"--- Error starting separate_scheduler: {e} ---")

    uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True, reload_dirs=["app"])
