"""REST API server (Flask + Waitress).

Serves standard endpoints for settings, logs, status, version,
authentication, and optional static UI assets.

Usage::

    from brinkhaustools.common import App
    from brinkhaustools.rest import RestServer

    app = App(name="my-service")
    app.start()

    server = RestServer(app=app, port=8080)
    server.start()
"""

import json
import logging
import os
from threading import Thread
from typing import Optional, Tuple

log = logging.getLogger(__name__)


def _lazy_flask():
    """Import Flask and friends lazily to keep the ``rest`` extra optional."""
    from flask import Flask, Response, request, send_from_directory, redirect
    from flask_cors import CORS  # noqa: F401
    from waitress import serve
    return Flask, Response, request, send_from_directory, redirect, CORS, serve


def _lazy_bcrypt():
    import bcrypt
    return bcrypt


class RestServer:
    """Flask + Waitress REST server for brinkhaustools.

    Parameters
    ----------
    app : App, optional
        Central ``App`` object.  If provided, components are read from it.
        Individual components can also be passed directly to override.
    settings : Settings, optional
    diagnosis : SelfDiagnosisEngine, optional
    status : StatusEngine, optional
    logging_helper : LoggingHelper, optional
    shutdown : ShutdownHandler, optional
    host : str
        IP to bind to.
    port : int
        Port to listen on.
    enable_auth : bool
        Whether authentication is required.
    username : str
        Username for login.
    default_password : str
        Default password (hashed on first use).
    ui_path : str, optional
        Directory with static UI assets.  ``None`` disables UI serving.
    description_path : str, optional
        Directory containing ``mainSettingsDescription.json``.
    version_path : str, optional
        Directory containing ``version.json``.
    """

    def __init__(
        self,
        app=None,
        settings=None,
        diagnosis=None,
        status=None,
        logging_helper=None,
        shutdown=None,
        host: str = "0.0.0.0",
        port: int = 8080,
        enable_auth: bool = True,
        username: str = "admin",
        default_password: str = "admin",
        ui_path: Optional[str] = None,
        description_path: Optional[str] = None,
        version_path: Optional[str] = None,
    ) -> None:
        # Resolve components from App or explicit params
        self._settings = settings or (app.settings if app else None)
        self._diagnosis = diagnosis or (app.diagnosis if app else None)
        self._status = status or (app.status if app else None)
        self._logging_helper = logging_helper or (app.logging if app else None)
        self._shutdown = shutdown or (app.shutdown if app else None)

        self._host = host
        self._port = port
        self._enable_auth = enable_auth
        self._username = username
        self._default_password = default_password
        self._ui_path = ui_path
        self._description_path = description_path
        self._version_path = version_path

        Flask, Response, request, send_from_directory, redirect, CORS, serve = _lazy_flask()
        bcrypt = _lazy_bcrypt()

        self._serve = serve
        self._bcrypt = bcrypt

        # Build Flask app
        self._flask_app = Flask(
            __name__,
            static_folder=os.path.join(ui_path, "assets") if ui_path else None,
        )

        self._init_password = None
        if self._settings:
            self._init_password = self._settings.get(
                ["expert", "restFullService", "restServicePassword"],
                self._hash_password(default_password),
            )

        self._register_routes(Flask, Response, request, send_from_directory, redirect)

        self._thread: Optional[Thread] = None

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------
    @property
    def flask_app(self):
        """Access the underlying Flask app for adding custom routes."""
        return self._flask_app

    def start(self) -> None:
        """Start the server on a background thread."""
        log.info("REST server starting on %s:%d", self._host, self._port)
        self._thread = Thread(
            target=self._run,
            name="RestServer",
            daemon=True,
        )
        self._thread.start()

    def stop(self) -> None:
        log.info("REST server stopping")
        # Waitress doesn't expose a clean stop; the daemon thread
        # will be terminated when the process exits.

    # ------------------------------------------------------------------
    # Internals
    # ------------------------------------------------------------------
    def _run(self) -> None:
        self._serve(self._flask_app, host=self._host, port=self._port)

    def _hash_password(self, password: str) -> str:
        bcrypt = self._bcrypt
        return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")

    def _verify_password(self, password: str, hashed: str) -> bool:
        bcrypt = self._bcrypt
        return bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8"))

    def _register_routes(self, Flask, Response, request, send_from_directory, redirect) -> None:
        app = self._flask_app

        # -- Login --
        @app.route("/api/login", methods=["POST"])
        def login():
            data = request.json or {}
            uname = str(data.get("username", ""))
            pwd = str(data.get("password", ""))
            hashed = self._get_stored_password()
            if not hashed or uname != self._username or not self._verify_password(pwd, hashed):
                return Response("Invalid credentials.", content_type="application/json", status=401)
            return Response("Successfully logged in", content_type="text/plain", status=200)

        # -- Password change --
        @app.route("/api/password", methods=["POST"])
        def change_password():
            data = request.json or {}
            old_pwd = str(data.get("oldPassword", ""))
            new_pwd = str(data.get("newPassword", ""))
            hashed = self._get_stored_password()
            if not hashed or not self._verify_password(old_pwd, hashed):
                return Response("Invalid password.", content_type="application/json", status=401)
            new_hash = self._hash_password(new_pwd)
            if self._settings:
                self._settings.set(["expert", "restFullService", "restServicePassword"], new_hash)
                self._settings.save()
            return Response("Successfully changed password", content_type="application/json", status=200)

        # -- Settings --
        @app.route("/api/setSettings", methods=["POST"])
        def set_settings():
            if self._settings:
                json_data = request.get_data().decode("utf-8")
                self._settings.load_json(json_data)
                return "Settings successfully updated", 200
            return "No settings configured", 404

        @app.route("/api/getSettings", methods=["GET"])
        def get_settings():
            if self._settings:
                return Response(self._settings.to_json(), content_type="application/json", status=200)
            return Response("{}", content_type="application/json", status=200)

        # -- Description --
        @app.route("/api/getDescription", methods=["GET"])
        def get_description():
            if self._description_path:
                try:
                    return send_from_directory(self._description_path, "mainSettingsDescription.json")
                except Exception as exc:
                    log.warning("Error serving description: %s", exc)
            return Response("{}", content_type="application/json", status=200)

        # -- Version --
        @app.route("/api/version", methods=["GET"])
        def get_version():
            if self._version_path:
                try:
                    return send_from_directory(self._version_path, "version.json")
                except Exception as exc:
                    log.warning("Error serving version: %s", exc)
            return Response("{}", content_type="application/json", status=200)

        # -- Logs --
        @app.route("/api/logs/getServiceLog", methods=["GET"])
        def get_service_log():
            if self._logging_helper:
                return Response(self._logging_helper.get_ring_buffer(), content_type="text/plain", status=200)
            return Response("", content_type="text/plain", status=200)

        @app.route("/api/logs/getMeasuringLog", methods=["GET"])
        def get_measuring_log():
            return Response("", content_type="text/plain", status=200)

        # -- Status --
        @app.route("/api/status/RuntimeTests", methods=["GET"])
        def get_diagnosis():
            if self._diagnosis:
                return Response(
                    json.dumps(self._diagnosis.get_status()),
                    content_type="application/json",
                    status=200,
                )
            return Response("{}", content_type="application/json", status=200)

        @app.route("/api/status/all", methods=["GET"])
        def get_all_status():
            if self._status:
                return Response(
                    json.dumps(self._status.get_last_status(), default=str),
                    content_type="application/json",
                    status=200,
                )
            return Response("{}", content_type="application/json", status=200)

        # -- Restart --
        @app.route("/api/restartApp", methods=["GET"])
        def restart_app():
            if self._shutdown:
                self._shutdown.trigger(0.5)
            return Response('{"result": 1}', content_type="application/json", status=200)

        # -- UI serving --
        if self._ui_path:
            @app.route("/")
            def serve_index():
                return send_from_directory(self._ui_path, "index.html")

            @app.route("/<path:path>")
            def catch_all(path):
                return redirect("/")

    def _get_stored_password(self) -> Optional[str]:
        if self._settings:
            return str(self._settings.get(
                ["expert", "restFullService", "restServicePassword"],
                self._init_password,
            ))
        return None
