use std::{
    fs::{create_dir_all, File},
    path::Path,
};

use anyhow::Result;
use rayon::prelude::*;

use crate::{
    fastapi::{
        core_files::{
            save_cache_file, save_config_file, save_core_utils_file, save_db_file,
            save_security_file,
        },
        docker_files::{
            save_dockercompose_file, save_dockercompose_override_file,
            save_dockercompose_traefik_file, save_dockerfile, save_dockerfileignore,
            save_entrypoint_script,
        },
        migration_files::save_initial_migrations,
        model_files::{save_message_model_file, save_token_models_file, save_user_models_file},
        route_files::{
            save_deps_file, save_health_route, save_login_route, save_router_file,
            save_users_route, save_version_route,
        },
        service_files::{save_cache_user_services_file, save_db_user_services_file},
        test_files::{
            save_config_test_file, save_conftest_file, save_health_route_test_file,
            save_login_route_test_file, save_main_test_file, save_test_deps_file,
            save_test_utils_file, save_user_model_test_file, save_user_routes_test_file,
            save_user_services_cache_test_file, save_user_services_db_test_file,
            save_version_route_test_file,
        },
    },
    file_manager::save_file_with_content,
    project_info::{DatabaseManager, ProjectInfo},
    utils::is_python_version_or_greater,
};

pub fn generate_fastapi(project_info: &ProjectInfo) -> Result<()> {
    create_directories(project_info)?;

    [
        save_cache_file,
        save_cache_user_services_file,
        save_config_test_file,
        save_conftest_file,
        save_db_file,
        save_db_user_services_file,
        save_dockercompose_file,
        save_dockercompose_override_file,
        save_dockercompose_traefik_file,
        save_dockerfileignore,
        save_dockerfile,
        save_entrypoint_script,
        save_example_env_file,
        save_exceptions_file,
        save_initial_migrations,
        save_main_file,
        save_main_test_file,
        save_message_model_file,
        save_config_file,
        save_core_utils_file,
        save_deps_file,
        save_health_route,
        save_health_route_test_file,
        save_login_route,
        save_login_route_test_file,
        save_router_file,
        save_security_file,
        save_test_deps_file,
        save_test_utils_file,
        save_token_models_file,
        save_types_file,
        save_user_models_file,
        save_user_model_test_file,
        save_users_route,
        save_user_routes_test_file,
        save_user_services_cache_test_file,
        save_user_services_db_test_file,
        save_version_route,
        save_version_route_test_file,
    ]
    .into_par_iter()
    .map(|f| f(project_info))
    .collect::<Result<Vec<_>, _>>()?;

    Ok(())
}

fn create_example_env_file(project_info: &ProjectInfo) -> String {
    let mut info = r#"SECRET_KEY=someKey
FIRST_SUPERUSER_EMAIL=some@email.com
FIRST_SUPERUSER_PASSWORD=changethis
FIRST_SUPERUSER_NAME="Wade Watts"
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=some_password
POSTGRES_DB=changethis
VALKEY_HOST=127.0.0.1
VALKEY_PASSWORD=test_password
STACK_NAME=changethis
DOMAIN=127.0.0.1
PRODUCTION_MODE=false
"#
    .to_string();

    if let Some(database_manager) = &project_info.database_manager {
        if database_manager == &DatabaseManager::AsyncPg {
            info.push_str("DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:{POSTGRES_PORT}/${POSTGRES_DB}\n");
        }
    }

    info
}

fn save_example_env_file(project_info: &ProjectInfo) -> Result<()> {
    let base = &project_info.base_dir();
    let file_path = base.join(".env-example");
    let file_content = create_example_env_file(project_info);

    save_file_with_content(&file_path, &file_content)?;

    Ok(())
}

fn create_exceptions_file() -> String {
    r#"class DbInsertError(Exception):
    pass


class DbUpdateError(Exception):
    pass


class NoDbPoolError(Exception):
    pass


class UserNotFoundError(Exception):
    pass
"#
    .to_string()
}

fn save_exceptions_file(project_info: &ProjectInfo) -> Result<()> {
    let base = &project_info.source_dir_path();
    let file_path = base.join("exceptions.py");
    let file_content = create_exceptions_file();

    save_file_with_content(&file_path, &file_content)?;

    Ok(())
}

fn create_main_file(project_info: &ProjectInfo) -> String {
    let module = &project_info.module_name();
    format!(
        r#"from __future__ import annotations

import sys
import traceback
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager

from fastapi import FastAPI, Request, Response
from loguru import logger
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import JSONResponse

from {module}.api.router import api_router
from {module}.core.cache import cache
from {module}.core.config import settings
from {module}.core.db import db

logger.remove()  # Remove the default logger so log level can be set
logger.add(sys.stderr, level=settings.LOG_LEVEL)


@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncGenerator:  # pragma: no cover
    logger.info("Initializing database connection pool")
    try:
        await db.create_pool()
    except Exception as e:
        logger.error(f"Error creating db connection pool: {{e}}")
        raise

    logger.info("Initializing cache client")
    try:
        await cache.create_client()
    except Exception as e:
        logger.error(f"Error creating cache client: {{e}}")
        raise

    logger.info("Saving first superuser")
    try:
        await db.create_first_superuser()
    except Exception as e:
        logger.error(f"Error creating first superuser: {{e}}")
        raise e

    yield

    logger.info("Closing database connection pool")
    try:
        await db.close_pool()
    except Exception as e:
        logger.error(f"Error closing db connection pool: {{e}}")
        raise

    logger.info("Closing cache client")
    try:
        await cache.close_client()
    except Exception as e:
        logger.error(f"Error closing cache client: {{e}}")
        raise


openapi_url = f"{{settings.API_V1_PREFIX}}/openapi.json"

app = FastAPI(
    title=settings.TITLE,
    lifespan=lifespan,
    openapi_url=openapi_url,
)


@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> Response:
    if exc.status_code >= 500:
        stack_trace = (
            "".join(
                traceback.format_exception(
                    type(exc.__cause__), exc.__cause__, exc.__cause__.__traceback__
                )
            )
            if exc.__cause__
            else traceback.format_exc()
        )

        original_exc_type = type(exc.__cause__).__name__ if exc.__cause__ else "HTTPException"
        original_exc_msg = str(exc.__cause__) if exc.__cause__ else str(exc.detail)

        msg = f"""HTTP {{exc.status_code}} error in {{request.method}} {{request.url.path}}\n
            Original exception: {{original_exc_type}}: {{original_exc_msg}}\n
            HTTP detail: {{exc.detail}}\n
            Stack trace:\n{{stack_trace}}"""

        logger.error(msg)

    return JSONResponse(status_code=exc.status_code, content={{"detail": exc.detail}})


if settings.all_cors_origins:
    app.add_middleware(
        CORSMiddleware,
        allow_origins=settings.all_cors_origins,
        allow_credentials=True,
        allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
        allow_headers=["Authorization", "Content-Type"],
    )

app.include_router(api_router)
"#
    )
}

fn save_main_file(project_info: &ProjectInfo) -> Result<()> {
    let base = project_info.source_dir_path();
    let file_path = base.join("main.py");
    let file_content = create_main_file(project_info);

    save_file_with_content(&file_path, &file_content)?;

    Ok(())
}

fn create_types_file(project_info: &ProjectInfo) -> Result<String> {
    if is_python_version_or_greater(&project_info.min_python_version, 12)? {
        Ok(r#"from typing import Any, Literal

type ActiveFilter = Literal["all", "active", "inactive"]
type Json = dict[str, Any]
"#
        .to_string())
    } else {
        Ok(r#"from typing import Any, Literal, TypeAlias

ActiveFilter: TypeAlias = Literal["all", "active", "inactive"]
Json: TypeAlias = dict[str, Any]
"#
        .to_string())
    }
}

fn save_types_file(project_info: &ProjectInfo) -> Result<()> {
    let base = project_info.source_dir_path();
    let file_path = base.join("types.py");
    let file_content = create_types_file(project_info)?;

    save_file_with_content(&file_path, &file_content)?;

    Ok(())
}

fn create_directories(project_info: &ProjectInfo) -> Result<()> {
    [
        create_api_dir,
        create_core_dir,
        create_migrations_dir,
        create_models_dir,
        create_scripts_dir,
        create_services_dir,
        create_test_dir,
    ]
    .into_par_iter()
    .map(|f| f(project_info))
    .collect::<Result<Vec<_>, _>>()?;

    Ok(())
}

fn create_api_dir(project_info: &ProjectInfo) -> Result<()> {
    let src = &project_info.source_dir_path();
    let api_dir = src.join("api");
    let routes_dir = api_dir.join("routes");
    create_dir_all(&routes_dir)?;
    save_init_file(&api_dir)?;
    save_init_file(&routes_dir)?;

    Ok(())
}

fn create_core_dir(project_info: &ProjectInfo) -> Result<()> {
    let src = &project_info.source_dir_path();
    let core_dir = src.join("core");
    create_dir_all(&core_dir)?;
    save_init_file(&core_dir)?;

    Ok(())
}

fn create_migrations_dir(project_info: &ProjectInfo) -> Result<()> {
    let base = project_info.base_dir();
    let migrations_dir = base.join("migrations");
    create_dir_all(migrations_dir)?;

    Ok(())
}

fn create_models_dir(project_info: &ProjectInfo) -> Result<()> {
    let src = &project_info.source_dir_path();
    let models_dir = src.join("models");
    create_dir_all(&models_dir)?;
    save_init_file(&models_dir)?;

    Ok(())
}

fn create_scripts_dir(project_info: &ProjectInfo) -> Result<()> {
    let src = &project_info.base_dir();
    let models_dir = src.join("scripts");
    create_dir_all(&models_dir)?;

    Ok(())
}

fn create_services_dir(project_info: &ProjectInfo) -> Result<()> {
    let src = &project_info.source_dir_path();
    let services_dir = src.join("services");
    let services_db_dir = services_dir.join("db");
    let services_cache_dir = services_dir.join("cache");
    create_dir_all(&services_db_dir)?;
    create_dir_all(&services_cache_dir)?;
    save_init_file(&services_dir)?;
    save_init_file(&services_db_dir)?;
    save_init_file(&services_cache_dir)?;

    Ok(())
}

fn create_test_dir(project_info: &ProjectInfo) -> Result<()> {
    let src = &project_info.base_dir();
    let test_dir = src.join("tests");
    let api_dir = test_dir.join("api");
    let routes_dir = api_dir.join("routes");
    let core_dir = test_dir.join("core");
    let models_dir = test_dir.join("models");
    let services_dir = test_dir.join("services");
    let services_db_dir = services_dir.join("db");
    let services_cache_dir = services_dir.join("cache");
    create_dir_all(&routes_dir)?;
    create_dir_all(&core_dir)?;
    create_dir_all(&models_dir)?;
    create_dir_all(&services_cache_dir)?;
    create_dir_all(&services_db_dir)?;
    save_init_file(&api_dir)?;
    save_init_file(&core_dir)?;
    save_init_file(&models_dir)?;
    save_init_file(&routes_dir)?;
    save_init_file(&services_dir)?;
    save_init_file(&services_db_dir)?;
    save_init_file(&services_cache_dir)?;

    Ok(())
}

fn save_init_file(path: &Path) -> Result<()> {
    let file_path = path.join("__init__.py");
    File::create(file_path)?;

    Ok(())
}
