import argparse
import os
import sys
import traceback
from pathlib import Path
from typing import Optional

import psutil
import uvicorn
from fasthtml.common import FileResponse, JSONResponse
from loguru import logger
from monsterui.core import FastHTML, Theme

from akkudoktoreos.config.config import get_config

# Pages
from akkudoktoreos.server.dash.admin import Admin
from akkudoktoreos.server.dash.bokeh import BokehJS
from akkudoktoreos.server.dash.components import Page
from akkudoktoreos.server.dash.configuration import ConfigKeyUpdate, Configuration
from akkudoktoreos.server.dash.demo import Demo
from akkudoktoreos.server.dash.footer import Footer
from akkudoktoreos.server.dash.hello import Hello
from akkudoktoreos.server.server import get_default_host, wait_for_port_free

config_eos = get_config()

# The favicon for EOSdash
favicon_filepath = Path(__file__).parent.joinpath("dash/assets/favicon/favicon.ico")
if not favicon_filepath.exists():
    raise ValueError(f"Does not exist {favicon_filepath}")

# Command line arguments
args: Optional[argparse.Namespace] = None


# Get frankenui and tailwind headers via CDN using Theme.green.headers()
# Add Bokeh headers
hdrs = (
    Theme.green.headers(highlightjs=True),
    BokehJS,
)

# The EOSdash application
app: FastHTML = FastHTML(
    title="EOSdash",
    hdrs=hdrs,
    secret_key=os.getenv("EOS_SERVER__EOSDASH_SESSKEY"),
)


def eos_server() -> tuple[str, int]:
    """Retrieves the EOS server host and port configuration.

    If `args` is provided, it uses the `eos_host` and `eos_port` from `args`.
    Otherwise, it falls back to the values from `config_eos.server`.

    Returns:
        tuple[str, int]: A tuple containing:
            - `eos_host` (str): The EOS server hostname or IP.
            - `eos_port` (int): The EOS server port.
    """
    if args is None:
        eos_host = str(config_eos.server.host)
        eos_port = config_eos.server.port
    else:
        eos_host = args.eos_host
        eos_port = args.eos_port
    eos_host = eos_host if eos_host else get_default_host()
    eos_port = eos_port if eos_port else 8503

    return eos_host, eos_port


@app.get("/favicon.ico")
def get_eosdash_favicon():  # type: ignore
    """Get favicon."""
    return FileResponse(path=favicon_filepath)


@app.get("/")
def get_eosdash():  # type: ignore
    """Serves the main EOSdash page.

    Returns:
        Page: The main dashboard page with navigation links and footer.
    """
    return Page(
        None,
        {
            "EOSdash": "/eosdash/hello",
            "Config": "/eosdash/configuration",
            "Demo": "/eosdash/demo",
            "Admin": "/eosdash/admin",
        },
        Hello(),
        Footer(*eos_server()),
        "/eosdash/footer",
    )


@app.get("/eosdash/footer")
def get_eosdash_footer():  # type: ignore
    """Serves the EOSdash Foooter information.

    Returns:
        Footer: The Footer component.
    """
    return Footer(*eos_server())


@app.get("/eosdash/hello")
def get_eosdash_hello():  # type: ignore
    """Serves the EOSdash Hello page.

    Returns:
        Hello: The Hello page component.
    """
    return Hello()


@app.get("/eosdash/admin")
def get_eosdash_admin():  # type: ignore
    """Serves the EOSdash Admin page.

    Returns:
        Admin: The Admin page component.
    """
    return Admin(*eos_server())


@app.post("/eosdash/admin")
def post_eosdash_admin(data: dict):  # type: ignore
    return Admin(*eos_server(), data)


@app.get("/eosdash/configuration")
def get_eosdash_configuration():  # type: ignore
    """Serves the EOSdash Configuration page.

    Returns:
        Configuration: The Configuration page component.
    """
    return Configuration(*eos_server())


@app.put("/eosdash/configuration")
def put_eosdash_configuration(data: dict):  # type: ignore
    return ConfigKeyUpdate(*eos_server(), data["key"], data["value"])


@app.get("/eosdash/demo")
def get_eosdash_demo():  # type: ignore
    """Serves the EOSdash Demo page.

    Returns:
        Demo: The Demo page component.
    """
    return Demo(*eos_server())


@app.get("/eosdash/health")
def get_eosdash_health():  # type: ignore
    """Health check endpoint to verify that the EOSdash server is alive."""
    return JSONResponse(
        {
            "status": "alive",
            "pid": psutil.Process().pid,
        }
    )


@app.get("/eosdash/assets/{fname:path}.{ext:static}")
def get_eosdash_assets(fname: str, ext: str):  # type: ignore
    """Get assets."""
    asset_filepath = Path(__file__).parent.joinpath(f"dash/assets/{fname}.{ext}")
    return FileResponse(path=asset_filepath)


def run_eosdash() -> None:
    """Run the EOSdash server with the specified configurations.

    This function starts the EOSdash server using the Uvicorn ASGI server. It accepts
    arguments for the host, port, log level, access log, and reload options. The
    log level is converted to lowercase to ensure compatibility with Uvicorn's
    expected log level format. If an error occurs while attempting to bind the
    server to the specified host and port, an error message is logged and the
    application exits.

    Returns:
        None
    """
    # Setup parameters from args, config_eos and default
    # Remember parameters that are also in config
    # - EOS host
    if args and args.eos_host:
        eos_host = args.eos_host
    elif config_eos.server.host:
        eos_host = config_eos.server.host
    else:
        eos_host = get_default_host()
    config_eos.server.host = eos_host
    # - EOS port
    if args and args.eos_port:
        eos_port = args.eos_port
    elif config_eos.server.port:
        eos_port = config_eos.server.port
    else:
        eos_port = 8503
    config_eos.server.port = eos_port
    # - EOSdash host
    if args and args.host:
        eosdash_host = args.host
    elif config_eos.server.eosdash.host:
        eosdash_host = config_eos.server.eosdash_host
    else:
        eosdash_host = get_default_host()
    config_eos.server.eosdash_host = eosdash_host
    # - EOS port
    if args and args.port:
        eosdash_port = args.port
    elif config_eos.server.eosdash_port:
        eosdash_port = config_eos.server.eosdash_port
    else:
        eosdash_port = 8504
    config_eos.server.eosdash_port = eosdash_port
    # - log level
    if args and args.log_level:
        log_level = args.log_level
    else:
        log_level = "info"
    # - access log
    if args and args.access_log:
        access_log = args.access_log
    else:
        access_log = False
    # - reload
    if args and args.reload:
        reload = args.reload
    else:
        reload = False

    # Make hostname Windows friendly
    if eosdash_host == "0.0.0.0" and os.name == "nt":  # noqa: S104
        eosdash_host = "localhost"

    # Wait for EOSdash port to be free - e.g. in case of restart
    wait_for_port_free(eosdash_port, timeout=120, waiting_app_name="EOSdash")

    try:
        uvicorn.run(
            "akkudoktoreos.server.eosdash:app",
            host=eosdash_host,
            port=eosdash_port,
            log_level=log_level.lower(),
            access_log=access_log,
            reload=reload,
        )
    except Exception as e:
        logger.error(f"Could not bind to host {eosdash_host}:{eosdash_port}. Error: {e}")
        raise e


def main() -> None:
    """Parse command-line arguments and start the EOSdash server with the specified options.

    This function sets up the argument parser to accept command-line arguments for
    host, port, log_level, access_log, and reload. It uses default values from the
    config module if arguments are not provided. After parsing the arguments,
    it starts the EOSdash server with the specified configurations.

    Command-line Arguments:
    --host (str): Host for the EOSdash server (default: value from config).
    --port (int): Port for the EOSdash server (default: value from config).
    --eos-host (str): Host for the EOS server (default: value from config).
    --eos-port (int): Port for the EOS server (default: value from config).
    --log_level (str): Log level for the server. Options: "critical", "error", "warning", "info", "debug", "trace" (default: "info").
    --access_log (bool): Enable or disable access log. Options: True or False (default: False).
    --reload (bool): Enable or disable auto-reload. Useful for development. Options: True or False (default: False).
    """
    parser = argparse.ArgumentParser(description="Start EOSdash server.")

    parser.add_argument(
        "--host",
        type=str,
        default=str(config_eos.server.eosdash_host),
        help="Host for the EOSdash server (default: value from config)",
    )
    parser.add_argument(
        "--port",
        type=int,
        default=config_eos.server.eosdash_port,
        help="Port for the EOSdash server (default: value from config)",
    )
    parser.add_argument(
        "--eos-host",
        type=str,
        default=str(config_eos.server.host),
        help="Host of the EOS server (default: value from config)",
    )
    parser.add_argument(
        "--eos-port",
        type=int,
        default=config_eos.server.port,
        help="Port of the EOS server (default: value from config)",
    )
    parser.add_argument(
        "--log_level",
        type=str,
        default="info",
        help='Log level for the server. Options: "critical", "error", "warning", "info", "debug", "trace" (default: "info")',
    )
    parser.add_argument(
        "--access_log",
        type=bool,
        default=False,
        help="Enable or disable access log. Options: True or False (default: False)",
    )
    parser.add_argument(
        "--reload",
        type=bool,
        default=False,
        help="Enable or disable auto-reload. Useful for development. Options: True or False (default: False)",
    )

    global args
    args = parser.parse_args()

    try:
        run_eosdash()
    except Exception as ex:
        error_msg = f"Failed to run EOSdash: {ex}"
        logger.error(error_msg)
        traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()
