# AUTOGENERATED FILE
from fastapi import FastAPI, Request, HTTPException, Header, Depends, UploadFile, File, Form, Query
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse, StreamingResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.staticfiles import StaticFiles
from starlette.responses import Response
from starlette.exceptions import HTTPException as StarletteHTTPException
from pathlib import Path
from typing import Dict, Any, List, Optional
import json
import re
from pydantic import BaseModel
from fastapi.middleware.cors import CORSMiddleware
from os import path as file_path
import time

import traceback
import asyncio
import tempfile
import uuid
import os
import shutil
import aiofiles


from naeural_core.utils.uvicorn_fast_api_ipc_manager import UvicornPluginComms


ADDITIONAL_FASTAPI_DATA = {{ additional_fastapi_data }}
BLOB_RESPONSE_TYPES = (FileResponse, StreamingResponse, Response)

def _with_additional_data(payload: Any):
  if isinstance(payload, BLOB_RESPONSE_TYPES):
    return payload
  merged = dict(ADDITIONAL_FASTAPI_DATA)
  if isinstance(payload, dict):
    merged.update(payload)
    return merged
  return {
    'result': payload,
    **ADDITIONAL_FASTAPI_DATA
  }

def _http_error(status_code: int, detail: Any):
  payload = detail if isinstance(detail, dict) else {"detail": detail}
  raise HTTPException(status_code=status_code, detail=_with_additional_data(payload))

eng = UvicornPluginComms(
  port={{ manager_port }},
  auth={{ manager_auth }},
  timeout_s={{ request_timeout }},
  additional_fastapi_data=ADDITIONAL_FASTAPI_DATA
)

app = FastAPI(
  title={{ api_title }},
  summary={{ api_summary }},
  description={{ api_description }},
  version={{ api_version }},
)

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

STATIC_DIR = "{{ static_directory }}"
DEBUG = {{ debug_web_app }}
{% if default_route is not none %}
DEFAULT_ROUTE = "{{ default_route }}"
{% else %}
DEFAULT_ROUTE = None
{% endif %}

PROFILE_RATE = {{ profile_rate }}
PROFILE_LOG_PER_REQUEST = {{ profile_log_per_request }}

"""
Profiling timings nomenclature (all timestamps are integers in nanoseconds).

This web server can emit sampled per-request timing profiles back to the
corresponding business plugin (`FastApiWebAppPlugin`) for aggregation/logging.
Most requests do not incur extra work; profiling is enabled only when
`PROFILE_RATE > 0`.

Clock domains
-------------
- `*_ns` fields use `time.perf_counter_ns()` (monotonic, suitable for durations).
- `*_wall_ns` fields use `time.time_ns()` (wall clock, used to estimate cross-process
  queueing delay on the same host).

Uvicorn/FastAPI-side markers
----------------------------
- `t_http_start_ns`: captured at the very start of the route handler.
- `t_before_call_plugin_ns`: right before IPC call (`eng.call_plugin(...)`).
- `t_after_call_plugin_ns`: immediately after IPC returns.
- `t_before_return_ns`: immediately before returning the HTTP response from the route.
- `t_upload_written_ns`: (upload endpoints only) after the uploaded payload is fully written to disk.

IPC markers (UvicornPluginComms)
-------------------------------
- `t_put_start_ns`, `t_put_end_ns`: duration of enqueueing the request to the IPC queue.
- `t_wait_start_ns`, `t_wait_end_ns`: duration spent waiting for the business plugin response.
- `t_put_wall_ns`: wall-clock time when the request was enqueued (used with plugin dequeue wall time).

Plugin-side markers (slice-aware for postponed requests)
-------------------------------------------------------
The plugin may return `PostponedRequest`, meaning the request completes over multiple
plugin-loop iterations. For this reason, execution time is represented as:
- `slice_count`: number of execution slices performed by the plugin for this request.
- `exec_total_ns`: sum of all slice durations in the plugin (monotonic).

Additionally, for queueing estimation on a single host:
- `t_plugin_dequeue_wall_ns`: wall-clock time when the plugin dequeued the request.
"""

# Define security scheme for Swagger UI
security = HTTPBearer()

def get_bearer_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str:
    """
    Extract the Bearer token using FastAPI's HTTPBearer dependency.

    Parameters
    ----------
    credentials : HTTPAuthorizationCredentials
        Automatically provided by FastAPI when using HTTPBearer.

    Returns
    -------
    str
        The extracted token.

    Raises
    ------
    HTTPException
        If the token is invalid or missing.
    """
    if DEBUG:
        print(f"Received authorization credentials: {credentials}")
    if not credentials:
        _http_error(401, "Authorization credentials are missing. Please provide a Bearer token.")
    if credentials.scheme.lower() != "bearer":
        _http_error(401, f"Invalid authorization scheme '{credentials.scheme}'. Expected 'Bearer'.")
    return credentials.credentials

def _should_profile() -> bool:
    if not isinstance(PROFILE_RATE, (int, float)) or PROFILE_RATE <= 0:
        return False
    if PROFILE_RATE >= 1:
        return True
    tail = uuid.uuid4().int & 0xFFFF
    return (tail / 0xFFFF) < PROFILE_RATE

def _start_profile(endpoint_name: str) -> dict | None:
    if not _should_profile():
        return None
    return {
        "endpoint": endpoint_name,
        "t_http_start_ns": time.perf_counter_ns(),
    }

async def _profiled_call_plugin(endpoint_name: str, call_plugin_args: List, profile: dict | None):
    if isinstance(profile, dict):
        profile["t_before_call_plugin_ns"] = time.perf_counter_ns()
    result = None
    status_code = None
    detail = None
    handled_exception = False
    try:
        result = await eng.call_plugin(
            endpoint_name,
            *call_plugin_args,
            profile=profile,
        )
        if isinstance(result, dict):
            exception_metadata = result.get("exception_metadata")
            status_code = result.get("status_code")
            if isinstance(exception_metadata, dict):
                exc = exception_metadata.get("exception")
                trace = exception_metadata.get("trace")
                detail = exception_metadata.get("detail")
                if not exception_metadata.get("logged"):
                    print(f"[ipc-error] endpoint={endpoint_name} req_id={profile.get('req_id') if isinstance(profile, dict) else ''} exc={exc}\n{trace}")
                handled_exception = True
                if isinstance(profile, dict):
                    profile["t_after_call_plugin_ns"] = time.perf_counter_ns()
                    _finalize_profile(profile, result)
                detail_payload = result.get("result") or "Internal plugin communication error"
                if result.get("logged") or exception_metadata.get("logged"):
                    detail_payload = {"detail": detail_payload, "logged": True}
                _http_error(status_code or 500, detail_payload)
            # endif exception metadata existent
        # endif result is dictionary
    except Exception as e:
        if not handled_exception:
            # Optionally log the exception or wrap it in an HTTPException
            log_str = f"[ipc-error] endpoint={endpoint_name} req_id={profile.get('req_id') if isinstance(profile, dict) else ''} exc={e}"
            log_str += traceback.format_exc()
            print(log_str)
            status_code = status_code or 500
            if isinstance(profile, dict):
                profile["t_after_call_plugin_ns"] = time.perf_counter_ns()
                _finalize_profile(profile, {"status_code": status_code, "result": str(e)})
            _http_error(status_code, f"Internal server error during plugin call: {e}")
        raise e  # re-raise if already handled
    if isinstance(profile, dict):
        profile["t_after_call_plugin_ns"] = time.perf_counter_ns()
    return result

def _finalize_profile(profile: dict | None, result: Any = None) -> None:
    if not isinstance(profile, dict):
        return
    profile["t_before_return_ns"] = time.perf_counter_ns()
    if isinstance(result, dict):
        status_code = result.get("status_code")
        if status_code is not None:
            profile["status_code"] = status_code
    asyncio.create_task(eng.send_profile_event(profile))
    return

def _handle_plugin_result(endpoint_name: str, result: Any, profile: dict | None):
    if not isinstance(result, dict):
        return result
    req_id = profile.get("req_id") if isinstance(profile, dict) else ""
    status_code = result.get("status_code")
    detail = result.get("result") or result.get("error") or result
    already_logged = result.get("logged")
    if status_code is not None:
        if not isinstance(status_code, int) or status_code < 200 or status_code > 299:
            if not already_logged:
                print(f"[plugin-status] endpoint={endpoint_name} req_id={req_id} status={status_code} detail={detail}")
            if not isinstance(detail, dict) and already_logged:
                detail = {"detail": detail, "logged": True}
            _http_error(int(status_code), detail)
    if "error" in result:
        if not already_logged:
            print(f"[plugin-error] endpoint={endpoint_name} req_id={req_id} detail={result.get('error')}")
        error_detail = result.get("error")
        if already_logged:
            error_detail = {"detail": error_detail, "logged": True}
        _http_error(500, error_detail)
    return result


{% for item in node_comm_params %}
{% if item['streaming_type'] == 'upload' %}
# Streaming upload endpoint
@app.post("/{{ item['name'] }}")
async def {{ item['name'] }}_upload(
    file: UploadFile = File(...),
    body: str = Form(None),
    {% if item['has_kwargs'] %}extras: str | None = Form(None),{% endif %}
    {% if item['require_token'] %}token: str = Depends(get_bearer_token){% endif %}
):
    """{{ item['endpoint_doc'] }}"""
    profile = _start_profile("{{ item['name'] }}")

    body_json = {}
    if body:
        try:
            body_json = json.loads(body)
        except Exception:
            _http_error(400, "Invalid JSON in form field 'body'")

    {% if item['has_kwargs'] %}
        extras_dict: Dict[str, Any] = {}
        if extras:
            try:
                extras_dict = json.loads(extras)
            except Exception:
                _http_error(400, "Invalid JSON in form field 'extras'")
    {% endif %}

    temp_dir = None
    file_path = None

    try:
        # Create a unique temp directory and file path
        temp_dir = tempfile.mkdtemp(prefix="upload_", dir="/tmp")
        safe_name = os.path.basename(file.filename or "")
        if not safe_name:
            safe_name = f"upload_{uuid.uuid4().hex}"
        base_dir = os.path.realpath(temp_dir)
        candidate_path = os.path.realpath(os.path.join(base_dir, safe_name))
        if os.path.commonpath([base_dir, candidate_path]) != base_dir:
            _http_error(400, "Invalid filename.")
        file_path = candidate_path

        # Save uploaded file stream chunk by chunk to disk
        chunk_size = {{ item['chunk_size'] }}
        with open(file_path, "wb") as f:
            while True:
                chunk = await file.read(chunk_size)
                if not chunk:
                    break
                f.write(chunk)
        if isinstance(profile, dict):
            profile["t_upload_written_ns"] = time.perf_counter_ns()

        # Prepare arguments for plugin call

        call_plugin_args: List = []
        {% if item['require_token'] %}call_plugin_args.append(token){% endif %}
        call_plugin_args += [file_path, body_json]

{#        call_plugin_kwargs: Dict[str, Any] = {}#}
{#        {% if item['has_kwargs'] %}call_plugin_kwargs.update(extras_dict){% endif %}#}
        {% if item['has_kwargs'] %}call_plugin_args.append(extras_dict){% endif %}

        result = await _profiled_call_plugin("{{ item['name'] }}", call_plugin_args, profile)

        _finalize_profile(profile, result)
        _handle_plugin_result("{{ item['name'] }}", result, profile)
        return _with_additional_data(result)

    except Exception as e:
        # Optionally log the exception or wrap it in an HTTPException
        print(f"Error during file upload endpoint: {e}")
        print(traceback.format_exc())
        if isinstance(profile, dict):
            _finalize_profile(profile, {"status_code": 500, "result": str(e)})
        _http_error(500, f"Internal server error: {e}")

    finally:
        # Cleanup: remove temp file and directory if they exist
        try:
            if file_path and os.path.isfile(file_path):
                os.remove(file_path)
            if temp_dir and os.path.isdir(temp_dir):
                shutil.rmtree(temp_dir)
        except Exception as cleanup_exc:
            print(f"Failed to cleanup temp upload files: {cleanup_exc}")


{% elif item['streaming_type'] == 'download' %}
# Streaming download endpoint
@app.get("/{{ item['name'] }}")
async def {{ item['name'] }}_download(
{#    {% for arg in item['args'] %}{{ arg }}, {% endfor %}#}
    {% if item['has_kwargs'] %}extras: str | None = Query(None),{% endif %}
    {% for arg in item['args'] %}{{ arg }}, {% endfor %}
    {% if item['require_token'] %}token: str = Depends(get_bearer_token){% endif %}
):
    """{{ item['endpoint_doc'] }}"""
    profile = _start_profile("{{ item['name'] }}")

    {% if item['has_kwargs'] %}
    kwargs_dict: dict = {}
    if extras:
        try:
            kwargs_dict = json.loads(extras)
        except Exception:
            _http_error(400, "Invalid JSON in query param 'extras'")
    {% endif %}

    call_plugin_args: List = []
    {% if item['require_token'] %}call_plugin_args.append(token){% endif %}
    {% for param in item['params'] %}
    call_plugin_args.append({{ param }})
    {% endfor %}
    {% if item['has_kwargs'] %}call_plugin_args.append(kwargs_dict){% endif %}

    response = await _profiled_call_plugin("{{ item['name'] }}", call_plugin_args, profile)
    _finalize_profile(profile, response)
    _handle_plugin_result("{{ item['name'] }}", response, profile)

    file_path = ''
    meta = {}

    if isinstance(response, dict):
        # If the plugin returns a dict, extract the file path and metadata
        result = response.get("result", {})
        file_path = result.get("file_path", "")
        meta = result.get("meta", {})

    resolved_path = os.path.realpath(file_path) if isinstance(file_path, str) else ""
    if not (isinstance(resolved_path, str) and os.path.isfile(resolved_path)):
        _http_error(404, "File not found!")

    meta_str = json.dumps(meta)

    async def file_generator():
        chunk_size = {{ item['chunk_size'] }}
        async with aiofiles.open(resolved_path, 'rb') as f:
            while True:
                chunk = await f.read(chunk_size)
                if not chunk:
                    break
                yield chunk

    headers = {
        "x-meta": meta_str
    }

    return StreamingResponse(file_generator(), media_type='application/octet-stream', headers=headers)

{% else %}
# Regular endpoint
{% if item['method']=='post' %}
class {{ item['name'] }}Model(BaseModel):
    {% if item['args'] %}
    {% for arg in item['args'] %}
    {{ arg }}
    {% endfor %}
    {% if item['has_kwargs'] %}extras: Dict[str, Any] | None = None{% endif %}
    {% else %}
    pass
    {% endif %}

@app.post("/{{ item['name'] }}")
async def {{ item['name'] }}(
    request: {{ item['name'] }}Model
    {% if item['require_token'] %}, token: str = Depends(get_bearer_token){% endif %}
):
    """{{ item['endpoint_doc'] }}"""
    profile = _start_profile("{{ item['name'] }}")
    {% for param in item['params'] %}
    {{ param }} = request.{{ param }}
    {% endfor %}

    {% if item['has_kwargs'] %}
    kwargs_dict = request.extras or {}
    {% endif %}

    # ---- build call arguments once, no nested ifs ----
    call_plugin_args: List = []
    {% if item['require_token'] %}call_plugin_args.append(token){% endif %}
    {% for param in item['params'] %}
    call_plugin_args.append({{ param }})
    {% endfor %}

{#    call_plugin_kwargs: Dict[str, Any] = {}#}
{#    {% if item['has_kwargs'] %}call_plugin_kwargs.update(kwargs_dict){% endif %}#}
    {% if item['has_kwargs'] %}call_plugin_args.append(kwargs_dict){% endif %}

    result = await _profiled_call_plugin("{{ item['name'] }}", call_plugin_args, profile)

    _finalize_profile(profile, result)
    _handle_plugin_result("{{ item['name'] }}", result, profile)
    {% if item['require_token'] %}
    if isinstance(result, dict) and (result.get("result") == "Unauthorized token" or result.get("status_code") == 401):
        _http_error(401, result.get("result", result))
    {% endif %}
    return _with_additional_data(result)
{% else %}
{% if item['method'] is not none %}@app.{{ item['method'] }}("/{{ item['name'] }}"){% endif %}
async def {{ item['name'] }}(
    {% if item['has_kwargs'] %}extras: str | None = Query(None), {% endif %}
    {% for arg in item['args'] %}
    {{ arg }}, {% if not loop.last %} {% endif %}
    {% endfor %}
    {% if item['require_token'] %}token: str = Depends(get_bearer_token){% endif %}
):
    """{{ item['endpoint_doc'] }}"""
    profile = _start_profile("{{ item['name'] }}")
    {% if item['has_kwargs'] %}
    kwargs_dict: Dict[str, Any] = {}
    if extras:
        try:
            kwargs_dict = json.loads(extras)
        except Exception:
            _http_error(400, "Invalid JSON in query param 'extras'")
    {% endif %}

    # ---- build call arguments once, no nested ifs ----
    call_plugin_args: List = []
    {% if item['require_token'] %}call_plugin_args.append(token){% endif %}
    {% for param in item['params'] %}
    call_plugin_args.append({{ param }})
    {% endfor %}

{#    call_plugin_kwargs: Dict[str, Any] = {}#}
{#    {% if item['has_kwargs'] %}call_plugin_kwargs.update(kwargs_dict){% endif %}#}
    {% if item['has_kwargs'] %}call_plugin_args.append(kwargs_dict){% endif %}

    result = await _profiled_call_plugin("{{ item['name'] }}", call_plugin_args, profile)

    _finalize_profile(profile, result)
    _handle_plugin_result("{{ item['name'] }}", result, profile)
    {% if item['require_token'] %}
    if isinstance(result, dict) and (result.get("result") == "Unauthorized token" or result.get("status_code") == 401):
        _http_error(401, result.get("result", result))
    {% endif %}
    return _with_additional_data(result)
{% endif %}
{% endif %} # endif item['streaming_type'] %}
{% endfor %}


{% for item in html_files %}
@app.{{ item['method'] }}('{{ item['route'] }}')
async def html_route{{ loop.index }}():
  return FileResponse(file_path.join(STATIC_DIR, '{{ item['name'] }}'))

{% endfor %}

HTML_ROUTE_TO_FILE = {
{% for item in html_files %}
    "{{ item['route'] }}": "{{ item['name'] }}",
{% endfor %}
}
DEFAULT_ROUTE_FILE = HTML_ROUTE_TO_FILE.get(DEFAULT_ROUTE) if DEFAULT_ROUTE is not None else None


@app.exception_handler(StarletteHTTPException)
async def custom_404_handler(request: Request, exc: StarletteHTTPException):
    if exc.status_code == 404:
        # Regex pattern to match files with specific extensions
        pattern = re.compile(r".*\.(js|png|jpg|css|jpeg|gif|svg|ico)$")
        if pattern.match(request.url.path):
            file_path = Path(STATIC_DIR) / request.url.path.strip("/")
            if file_path.exists() and file_path.is_file():
                return FileResponse(file_path)

    if DEFAULT_ROUTE is not None and request.url.path != DEFAULT_ROUTE:
        if DEFAULT_ROUTE_FILE is not None:
            return FileResponse(file_path.join(STATIC_DIR, DEFAULT_ROUTE_FILE))
        return RedirectResponse(url=DEFAULT_ROUTE, status_code=307)
    # endif DEFAULT_ROUTE defined

    should_log = True
    if isinstance(exc.detail, dict) and exc.detail.get("logged"):
        should_log = False
    if exc.status_code < 500:
        should_log = False
    if should_log:
        print(f"StarletteHTTPException during request for {request.url}:\n{exc}")
    return JSONResponse(
        status_code=exc.status_code,
        content={"detail": exc.detail or "An error occurred."},
    )

app.mount(f"/{STATIC_DIR}", StaticFiles(directory=STATIC_DIR), name="static")
