import logging
import os
import sys
from logging.config import dictConfig
from pythonjsonlogger.json import JsonFormatter
from pathlib import Path
from datetime import datetime, timezone


class ISOJsonFormatter(JsonFormatter):
    def formatTime(self, record, datefmt=None):
        dt = datetime.fromtimestamp(record.created, tz=timezone.utc)
        return dt.isoformat(timespec="milliseconds").replace("+00:00", "Z")


LOG_FILE = Path(os.getenv("LOG_FILE", "/app/config/media_manager.log"))
LOGGING_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "default": {
            "format": "%(asctime)s - %(levelname)s - %(name)s - %(funcName)s(): %(message)s"
        },
        "json": {
            "()": ISOJsonFormatter,
            "format": "%(asctime)s %(levelname)s %(name)s %(message)s",
            "rename_fields": {
                "levelname": "level",
                "asctime": "timestamp",
                "name": "module",
            },
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "default",
            "stream": sys.stdout,
        },
        "file": {
            "class": "logging.handlers.RotatingFileHandler",
            "formatter": "json",
            "filename": str(LOG_FILE),
            "maxBytes": 10485760,
            "backupCount": 5,
            "encoding": "utf-8",
        },
    },
    "root": {
        "level": "DEBUG",
        "handlers": ["console", "file"],
    },
    "loggers": {
        "uvicorn": {"handlers": ["console", "file"], "level": "DEBUG"},
        "uvicorn.access": {"handlers": ["console", "file"], "level": "DEBUG"},
        "fastapi": {"handlers": ["console", "file"], "level": "DEBUG"},
    },
}
dictConfig(LOGGING_CONFIG)

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s - %(levelname)s - %(name)s - %(funcName)s(): %(message)s",
    stream=sys.stdout,
)

logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("transmission_rpc").setLevel(logging.WARNING)
logging.getLogger("qbittorrentapi").setLevel(logging.WARNING)
logging.getLogger("sabnzbd_api").setLevel(logging.WARNING)

log = logging.getLogger(__name__)

from psycopg.errors import UniqueViolation  # noqa: E402
from sqlalchemy.exc import IntegrityError  # noqa: E402
from media_manager.config import AllEncompassingConfig  # noqa: E402
import media_manager.torrent.router as torrent_router  # noqa: E402
import media_manager.movies.router as movies_router  # noqa: E402
import media_manager.tv.router as tv_router  # noqa: E402
from media_manager.tv.service import (  # noqa: E402
    auto_download_all_approved_season_requests,
    import_all_show_torrents,
    update_all_non_ended_shows_metadata,
)
from media_manager.movies.service import (  # noqa: E402
    import_all_movie_torrents,
    update_all_movies_metadata,
    auto_download_all_approved_movie_requests,
)
from media_manager.notification.router import router as notification_router  # noqa: E402
import uvicorn  # noqa: E402
from fastapi.staticfiles import StaticFiles  # noqa: E402
from media_manager.auth.router import users_router as custom_users_router  # noqa: E402
from media_manager.auth.router import auth_metadata_router  # noqa: E402
from media_manager.auth.schemas import UserCreate, UserRead, UserUpdate  # noqa: E402
from media_manager.auth.router import get_openid_router  # noqa: E402

from media_manager.auth.users import (  # noqa: E402
    bearer_auth_backend,
    fastapi_users,
    cookie_auth_backend,
    create_default_admin_user,
)
from media_manager.exceptions import (  # noqa: E402
    NotFoundError,
    not_found_error_exception_handler,
    MediaAlreadyExists,
    media_already_exists_exception_handler,
    InvalidConfigError,
    invalid_config_error_exception_handler,
    sqlalchemy_integrity_error_handler,
)

from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore  # noqa: E402
from starlette.responses import FileResponse, RedirectResponse  # noqa: E402

import media_manager.database  # noqa: E402
import shutil  # noqa: E402
from fastapi import FastAPI, APIRouter  # noqa: E402
from fastapi.middleware.cors import CORSMiddleware  # noqa: E402
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware  # noqa: E402
from starlette.responses import Response  # noqa: E402
from contextlib import asynccontextmanager  # noqa: E402
from apscheduler.schedulers.background import BackgroundScheduler  # noqa: E402
from apscheduler.triggers.cron import CronTrigger  # noqa: E402
from media_manager.database import init_engine  # noqa: E402

config = AllEncompassingConfig()

if config.misc.development:
    log.warning("Development Mode activated!")
else:
    log.info("Development Mode not activated!")


def hourly_tasks():
    log.info(f"Hourly tasks are running at {datetime.now()}")
    auto_download_all_approved_season_requests()
    import_all_show_torrents()
    import_all_movie_torrents()


def weekly_tasks():
    log.info(f"Weekly tasks are running at {datetime.now()}")
    update_all_non_ended_shows_metadata()
    update_all_movies_metadata()


init_engine(config.database)

jobstores = {"default": SQLAlchemyJobStore(engine=media_manager.database.engine)}

scheduler = BackgroundScheduler(jobstores=jobstores)
every_15_minutes_trigger = CronTrigger(minute="*/15", hour="*")
daily_trigger = CronTrigger(hour=0, minute=0, jitter=60 * 60 * 24 * 2)
weekly_trigger = CronTrigger(
    day_of_week="mon", hour=0, minute=0, jitter=60 * 60 * 24 * 2
)

scheduler.add_job(
    import_all_movie_torrents,
    every_15_minutes_trigger,
    id="import_all_movie_torrents",
    replace_existing=True,
)
scheduler.add_job(
    import_all_show_torrents,
    every_15_minutes_trigger,
    id="import_all_show_torrents",
    replace_existing=True,
)
scheduler.add_job(
    auto_download_all_approved_season_requests,
    daily_trigger,
    id="auto_download_all_approved_season_requests",
    replace_existing=True,
)
scheduler.add_job(
    auto_download_all_approved_movie_requests,
    daily_trigger,
    id="auto_download_all_approved_movie_requests",
    replace_existing=True,
)
scheduler.add_job(
    update_all_movies_metadata,
    weekly_trigger,
    id="update_all_movies_metadata",
    replace_existing=True,
)
scheduler.add_job(
    update_all_non_ended_shows_metadata,
    weekly_trigger,
    id="update_all_non_ended_shows_metadata",
    replace_existing=True,
)
scheduler.start()


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup: Create default admin user if needed
    await create_default_admin_user()
    yield
    # Shutdown
    scheduler.shutdown()


BASE_PATH = os.getenv("BASE_PATH", "")
FRONTEND_FILES_DIR = os.getenv("FRONTEND_FILES_DIR")
DISABLE_FRONTEND_MOUNT = os.getenv("DISABLE_FRONTEND_MOUNT", "").lower() in [
    "true",
    "1",
    "yes",
]


app = FastAPI(lifespan=lifespan, root_path=BASE_PATH)
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")

origins = config.misc.cors_urls
log.info(f"CORS URLs activated for following origins: {origins}")
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=[
        "GET",
        "PUT",
        "POST",
        "DELETE",
        "PATCH",
        "HEAD",
        "OPTIONS",
    ],
)

api_app = APIRouter(prefix="/api/v1")

# ----------------------------
# Hello World Router
# ----------------------------


@api_app.get("/health")
async def hello_world() -> dict:
    """
    A simple endpoint to check if the API is running.
    """
    return {"message": "Hello World!", "version": os.getenv("PUBLIC_VERSION")}


# ----------------------------
# Standard Auth Routers
# ----------------------------

api_app.include_router(
    fastapi_users.get_auth_router(bearer_auth_backend),
    prefix="/auth/jwt",
    tags=["auth"],
)
api_app.include_router(
    fastapi_users.get_auth_router(cookie_auth_backend),
    prefix="/auth/cookie",
    tags=["auth"],
)
api_app.include_router(
    fastapi_users.get_register_router(UserRead, UserCreate),
    prefix="/auth",
    tags=["auth"],
)
api_app.include_router(
    fastapi_users.get_reset_password_router(),
    prefix="/auth",
    tags=["auth"],
)
api_app.include_router(
    fastapi_users.get_verify_router(UserRead),
    prefix="/auth",
    tags=["auth"],
)

# ----------------------------
# User Management Routers
# ----------------------------

api_app.include_router(custom_users_router, tags=["users"])
api_app.include_router(
    fastapi_users.get_users_router(UserRead, UserUpdate),
    prefix="/users",
    tags=["users"],
)

# ----------------------------
# OpenID Connect Routers
# ----------------------------

api_app.include_router(auth_metadata_router, tags=["openid"])
if get_openid_router():
    api_app.include_router(get_openid_router(), tags=["openid"], prefix="/auth/oauth")

api_app.include_router(tv_router.router, prefix="/tv", tags=["tv"])
api_app.include_router(torrent_router.router, prefix="/torrent", tags=["torrent"])
api_app.include_router(movies_router.router, prefix="/movies", tags=["movie"])
api_app.include_router(
    notification_router, prefix="/notification", tags=["notification"]
)

app.mount(
    "/api/v1/static/image",
    StaticFiles(directory=config.misc.image_directory),
    name="static-images",
)

app.include_router(api_app)

# ----------------------------
# Frontend mounting (disabled in development)
# ----------------------------

if not DISABLE_FRONTEND_MOUNT:
    app.mount(
        "/web", StaticFiles(directory=FRONTEND_FILES_DIR, html=True), name="frontend"
    )
    log.info(f"Mounted frontend at /web from {FRONTEND_FILES_DIR}")
else:
    log.info("Frontend mounting disabled (DISABLE_FRONTEND_MOUNT is set)")

# ----------------------------
# Redirects to frontend
# ----------------------------


@app.get("/")
async def root():
    return RedirectResponse(url="/web/")


@app.get("/dashboard")
async def dashboard():
    return RedirectResponse(url="/web/")


@app.get("/login")
async def login():
    return RedirectResponse(url="/web/")


# ----------------------------
# Custom Exception Handlers
# ----------------------------

app.add_exception_handler(NotFoundError, not_found_error_exception_handler)
app.add_exception_handler(MediaAlreadyExists, media_already_exists_exception_handler)
app.add_exception_handler(InvalidConfigError, invalid_config_error_exception_handler)
app.add_exception_handler(IntegrityError, sqlalchemy_integrity_error_handler)
app.add_exception_handler(UniqueViolation, sqlalchemy_integrity_error_handler)


@app.exception_handler(404)
async def not_found_handler(request, exc):
    if not DISABLE_FRONTEND_MOUNT and any(
        base_path in ["/web", "/dashboard", "/login"] for base_path in request.url.path
    ):
        return FileResponse(f"{FRONTEND_FILES_DIR}/404.html")
    return Response(content="Not Found", status_code=404)


# ----------------------------
# Hello World
# ----------------------------

log.info("Hello World!")

# ----------------------------
# Startup filesystem checks
# ----------------------------
try:
    log.info("Creating directories if they don't exist...")
    config.misc.tv_directory.mkdir(parents=True, exist_ok=True)
    config.misc.movie_directory.mkdir(parents=True, exist_ok=True)
    config.misc.torrent_directory.mkdir(parents=True, exist_ok=True)
    config.misc.image_directory.mkdir(parents=True, exist_ok=True)

    log.info("Conducting filesystem tests...")
    test_dir = config.misc.tv_directory / Path(".media_manager_test_dir")
    test_dir.mkdir(parents=True, exist_ok=True)
    test_dir.rmdir()
    log.info(f"Successfully created test dir in TV directory at: {test_dir}")

    test_dir = config.misc.movie_directory / Path(".media_manager_test_dir")
    test_dir.mkdir(parents=True, exist_ok=True)
    test_dir.rmdir()
    log.info(f"Successfully created test dir in Movie directory at: {test_dir}")

    test_dir = config.misc.image_directory / Path(".media_manager_test_dir")
    test_dir.touch()
    test_dir.unlink()
    log.info(f"Successfully created test file in Image directory at: {test_dir}")

    # check if hardlink creation works
    test_dir = config.misc.tv_directory / Path(".media_manager_test_dir")
    test_dir.mkdir(parents=True, exist_ok=True)

    torrent_dir = config.misc.torrent_directory / Path(".media_manager_test_dir")
    torrent_dir.mkdir(parents=True, exist_ok=True)

    test_torrent_file = torrent_dir / Path(".media_manager.test.torrent")
    test_torrent_file.touch()

    test_hardlink = test_dir / Path(".media_manager.test.hardlink")
    try:
        test_hardlink.hardlink_to(test_torrent_file)
        if not test_hardlink.samefile(test_torrent_file):
            log.critical("Hardlink creation failed!")
        log.info("Successfully created test hardlink in TV directory")
    except OSError as e:
        log.error(
            f"Hardlink creation failed, falling back to copying files. Error: {e}"
        )
        shutil.copy(src=test_torrent_file, dst=test_hardlink)
    finally:
        test_hardlink.unlink()
        test_torrent_file.unlink()
        torrent_dir.rmdir()
        test_dir.rmdir()

except Exception as e:
    log.error(f"Error creating test directory: {e}")
    raise


if __name__ == "__main__":
    uvicorn.run(
        app,
        host="127.0.0.1",
        port=5049,
        log_config=LOGGING_CONFIG,
        proxy_headers=True,
        forwarded_allow_ips="*",
    )
