#!/usr/bin/env python3

"""
Contact - A Console UI for Meshtastic by http://github.com/pdxlocations
Powered by Meshtastic.org

Meshtastic® is a registered trademark of Meshtastic LLC.
Meshtastic software components are released under various licenses—see GitHub for details.
No warranty is provided. Use at your own risk.
"""

# Standard library
import contextlib
import curses
import io
import logging
import os
import subprocess
import sys
import threading
import traceback
from typing import Optional

# Third-party
from pubsub import pub

# Local application
import contact.ui.default_config as config
from contact.message_handlers.rx_handler import on_receive
from contact.settings import set_region
from contact.ui.colors import setup_colors
from contact.ui.contact_ui import main_ui
from contact.ui.splash import draw_splash
from contact.utilities.arg_parser import setup_parser
from contact.utilities.db_handler import init_nodedb, load_messages_from_db
from contact.utilities.demo_data import build_demo_interface, configure_demo_database, seed_demo_messages
from contact.utilities.input_handlers import get_list_input
from contact.utilities.i18n import t
from contact.ui.dialog import dialog
from contact.utilities.interfaces import initialize_interface, reconnect_interface
from contact.utilities.utils import get_channels, get_nodeNum, get_node_list
from contact.utilities.singleton import ui_state, interface_state, app_state

# ------------------------------------------------------------------------------
# Environment & Logging Setup
# ------------------------------------------------------------------------------

os.environ["NCURSES_NO_UTF8_ACS"] = "1"
os.environ["LANG"] = "C.UTF-8"
os.environ.setdefault("TERM", "xterm-256color")
if os.environ.get("COLORTERM") == "gnome-terminal":
    os.environ["TERM"] = "xterm-256color"

logging.basicConfig(
    filename=config.log_file_path, level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)

app_state.lock = threading.Lock()

DEFAULT_CLOSE_TIMEOUT_SECONDS = 5.0


# ------------------------------------------------------------------------------
# Main Program Logic
# ------------------------------------------------------------------------------
def prompt_region_if_unset(args: object, stdscr: Optional[curses.window] = None) -> None:
    """Prompt user to set region if it is unset."""
    confirmation = get_list_input("Your region is UNSET. Set it now?", "Yes", ["Yes", "No"])
    if confirmation == "Yes":
        set_region(interface_state.interface)
        close_interface(interface_state.interface)
        if stdscr is not None:
            draw_splash(stdscr)
        interface_state.interface = reconnect_interface(args)


def close_interface(interface: object, timeout_seconds: float = DEFAULT_CLOSE_TIMEOUT_SECONDS) -> bool:
    if interface is None:
        return True

    close_errors = []

    def _close_target() -> None:
        try:
            interface.close()
        except BaseException as error:  # Keep shutdown resilient even for KeyboardInterrupt/SystemExit from libraries.
            close_errors.append(error)

    close_thread = threading.Thread(target=_close_target, name="meshtastic-interface-close", daemon=True)
    close_thread.start()
    close_thread.join(timeout_seconds)

    if close_thread.is_alive():
        logging.warning("Timed out closing interface after %.1fs; continuing shutdown", timeout_seconds)
        return False

    if not close_errors:
        return True

    error = close_errors[0]
    if isinstance(error, KeyboardInterrupt):
        logging.info("Interrupted while closing interface; continuing shutdown")
        return True

    logging.warning("Ignoring error while closing interface: %r", error)
    return True


def interface_is_ready(interface: object) -> bool:
    try:
        return getattr(interface, "localNode", None) is not None and interface.localNode.localConfig is not None
    except Exception:
        return False


def initialize_runtime_interface_with_retry(stdscr: curses.window, args: object):
    while True:
        interface = initialize_runtime_interface(args)
        if getattr(args, "demo_screenshot", False) or interface_is_ready(interface):
            return interface

        choice = get_list_input(
            t("ui.prompt.node_not_found", default="No node found. Retry connection?"),
            "Retry",
            ["Retry", "Close"],
            mandatory=True,
        )
        close_interface(interface)
        if choice == "Close":
            return None

        draw_splash(stdscr)


def initialize_globals(seed_demo: bool = False) -> None:
    """Initializes interface and shared globals."""

    ui_state.channel_list = []
    ui_state.all_messages = {}
    ui_state.notifications = []
    ui_state.packet_buffer = []
    ui_state.node_list = []
    ui_state.selected_channel = 0
    ui_state.selected_message = 0
    ui_state.selected_node = 0
    ui_state.start_index = [0, 0, 0]
    interface_state.myNodeNum = get_nodeNum()
    ui_state.channel_list = get_channels()
    ui_state.node_list = get_node_list()
    ui_state.single_pane_mode = config.single_pane_mode.lower() == "true"
    pub.subscribe(on_receive, "meshtastic.receive")

    init_nodedb()
    if seed_demo:
        seed_demo_messages()
    load_messages_from_db()


def initialize_runtime_interface(args: object):
    if getattr(args, "demo_screenshot", False):
        configure_demo_database()
        return build_demo_interface()
    return initialize_interface(args)


def main(stdscr: curses.window) -> None:
    """Main entry point for the curses UI."""

    output_capture = io.StringIO()
    try:
        setup_colors()
        ensure_min_rows(stdscr)
        draw_splash(stdscr)

        args = setup_parser().parse_args()

        if getattr(args, "settings", False):
            subprocess.run([sys.executable, "-m", "contact.settings"], check=True)
            return

        logging.info("Initializing interface...")
        with app_state.lock:
            interface_state.interface = initialize_runtime_interface_with_retry(stdscr, args)
            if interface_state.interface is None:
                return

            if not getattr(args, "demo_screenshot", False) and interface_state.interface.localNode.localConfig.lora.region == 0:
                prompt_region_if_unset(args, stdscr)

            initialize_globals(seed_demo=getattr(args, "demo_screenshot", False))
            logging.info("Starting main UI")

            stdscr.clear()
            stdscr.refresh()

        try:
            with contextlib.redirect_stdout(output_capture), contextlib.redirect_stderr(output_capture):
                main_ui(stdscr)
        except Exception:
            console_output = output_capture.getvalue()
            logging.error("Uncaught exception inside main_ui")
            logging.error("Traceback:\n%s", traceback.format_exc())
            logging.error("Console output:\n%s", console_output)
            return

    except Exception:
        raise


def ensure_min_rows(stdscr: curses.window, min_rows: int = 11) -> None:
    while True:
        rows, _ = stdscr.getmaxyx()
        if rows >= min_rows:
            return
        dialog(
            t("ui.dialog.resize_title", default="Resize Terminal"),
            t(
                "ui.dialog.resize_body",
                default="Please resize the terminal to at least {rows} rows.",
                rows=min_rows,
            ),
        )
        curses.update_lines_cols()
        stdscr.clear()
        stdscr.refresh()


def start() -> None:
    """Entry point for the application."""

    if "--help" in sys.argv or "-h" in sys.argv:
        setup_parser().print_help()
        sys.exit(0)

    interrupted = False
    fatal_error = None

    try:
        curses.wrapper(main)
    except KeyboardInterrupt:
        interrupted = True
        logging.info("User exited with Ctrl+C")
    except Exception as e:
        fatal_error = e
        logging.critical("Fatal error", exc_info=True)
        try:
            curses.endwin()
        except Exception:
            pass
    finally:
        close_interface(interface_state.interface)

    if fatal_error is not None:
        print("Fatal error:", fatal_error)
        traceback.print_exc()
        sys.exit(1)

    if interrupted:
        sys.exit(0)


if __name__ == "__main__":
    start()
