from __future__ import annotations

import argparse
import importlib
import json
import signal
import sys
import time
from dataclasses import asdict
from typing import Optional

from .core.bus import CANBusEmulation
from .core.can import REPLInterface, SocketCANInterface, UnsupportedConfiguration
from .setup import Setup
from .ui.status import StatusHub


def _load_ecu_spec_name(path: Optional[str], fallback: str) -> str:
    if not path:
        return fallback
    try:
        with open(path, "r", encoding="utf-8") as handle:
            data = json.load(handle)
        name = data.get("name")
        if isinstance(name, str) and name.strip():
            return name.strip()
    except Exception:
        return fallback
    return fallback


def format_arbitration_table(ecus: dict, path_map: dict) -> str:
    """Generate an ASCII table showing ECU roles, names, and their arbitration IDs."""
    if not ecus:
        return "No ECUs loaded"
    
    # Collect data for table
    rows = []
    for name, ecu in sorted(ecus.items()):
        role = name
        ecu_name = _load_ecu_spec_name(path_map.get(name), fallback=name)
        for handler in ecu._handlers:
            arb_id = f"0x{handler.arbitration_id:03X}"
            reply_id = f"0x{handler.reply_id:03X}"
            
            # Include auxiliary IDs if present
            aux_ids = ""
            if handler.auxiliary_ids:
                aux_ids = ", ".join(f"0x{aid:03X}" for aid in sorted(handler.auxiliary_ids))
            
            rows.append({
                'role': role,
                'ecu_name': ecu_name,
                'arb_id': arb_id,
                'reply_id': reply_id,
                'aux_ids': aux_ids
            })
    
    # Calculate column widths
    role_width = max(len("Role"), max(len(row['role']) for row in rows))
    name_width = max(len("ECU Name"), max(len(row['ecu_name']) for row in rows))
    arb_width = max(len("Request ID"), max(len(row['arb_id']) for row in rows))
    reply_width = max(len("Reply ID"), max(len(row['reply_id']) for row in rows))
    aux_width = max(len("Auxiliary IDs"), max(len(row['aux_ids']) for row in rows)) if any(row['aux_ids'] for row in rows) else 0
    
    # Build table
    lines = []
    
    # Header
    lines.append("┌" + "─" * (role_width + 2) + "┬" + "─" * (name_width + 2) + "┬" + "─" * (arb_width + 2) + "┬" + "─" * (reply_width + 2) + ("┬" + "─" * (aux_width + 2) if aux_width else "") + "┐")
    
    header = f"│ {'Role':<{role_width}} │ {'ECU Name':<{name_width}} │ {'Request ID':<{arb_width}} │ {'Reply ID':<{reply_width}} "
    if aux_width:
        header += f"│ {'Auxiliary IDs':<{aux_width}} "
    header += "│"
    lines.append(header)
    
    lines.append("├" + "─" * (role_width + 2) + "┼" + "─" * (name_width + 2) + "┼" + "─" * (arb_width + 2) + "┼" + "─" * (reply_width + 2) + ("┼" + "─" * (aux_width + 2) if aux_width else "") + "┤")
    
    # Data rows
    for row in rows:
        row_line = f"│ {row['role']:<{role_width}} │ {row['ecu_name']:<{name_width}} │ {row['arb_id']:<{arb_width}} │ {row['reply_id']:<{reply_width}} "
        if aux_width:
            row_line += f"│ {row['aux_ids']:<{aux_width}} "
        row_line += "│"
        lines.append(row_line)
    
    # Footer
    lines.append("└" + "─" * (role_width + 2) + "┴" + "─" * (name_width + 2) + "┴" + "─" * (arb_width + 2) + "┴" + "─" * (reply_width + 2) + ("┴" + "─" * (aux_width + 2) if aux_width else "") + "┘")
    
    return "\n".join(lines)


def _ensure_optional_dependencies(args: argparse.Namespace) -> None:
    missing = []
    if getattr(args, "attach", None):
        from .core import can as can_module
        attach_target = args.attach.split("@", 1)[0]
        for package in can_module.missing_python_can_modules(attach_target):
            if package == "python-can":
                missing.append("python-can (required for --attach)")
            elif package == "gs_usb":
                missing.append("gs_usb (required for --attach when using gs_usb backend)")
            elif package == "cancorder-utils":
                missing.append("cancorder-utils (required for --attach when using toucan backend)")
            else:
                missing.append(f"{package} (required for --attach)")
    if getattr(args, "ecuconnect", False):
        if importlib.util.find_spec("lz4") is None:
            missing.append("lz4 (required for --ecuconnect)")
    if getattr(args, "logger", False):
        if importlib.util.find_spec("cancorder_utils") is None:
            missing.append("cancorder-utils (required for --logger)")
        if importlib.util.find_spec("zeroconf") is None:
            missing.append("zeroconf (required for --logger)")
    enet_enabled = bool(getattr(args, "enet_service_broker", False))
    enet_url_override = str(getattr(args, "enet_service_broker_url", "") or "").strip()
    if enet_enabled and not enet_url_override:
        if importlib.util.find_spec("zeroconf") is None:
            missing.append("zeroconf (required for --enet-service-broker auto-discovery)")
    if missing:
        details = "\n".join(f"- {item}" for item in missing)
        sys.exit(f"Missing optional dependencies:\n{details}\nInstall with `pip install -r requirements.txt`.")


def _parse_tester_address(value: str) -> int:
    try:
        address = int(value, 0)
    except ValueError as exc:
        raise argparse.ArgumentTypeError("DoIP tester address must be a number (e.g. 0x0E80)") from exc
    if not (0 <= address <= 0xFFFF):
        raise argparse.ArgumentTypeError("DoIP tester address must be between 0x0000 and 0xFFFF")
    return address


def _resolve_vin_from_specs(path_map: dict) -> Optional[str]:
    for _name, path in sorted(path_map.items()):
        try:
            with open(path, "r", encoding="utf-8") as handle:
                data = json.load(handle)
        except Exception:
            continue
        vin = data.get("vin")
        if not vin and isinstance(data.get("uds"), dict):
            vin = data["uds"].get("vin")
        if not vin and isinstance(data.get("kwp"), dict):
            vin = data["kwp"].get("vin")
        if isinstance(vin, str) and vin.strip():
            return vin.strip()
    return None


def main(argv: list[str] | None = None) -> None:
    parser = argparse.ArgumentParser(prog="ecumulator", description="Electronic Control Unit Emulator")
    parser.add_argument(
        "specs",
        nargs="*",
        help="Load a specification in the format <name>:<path-to-json> or <vehicle(.vehicle|.json)>",
    )
    parser.add_argument("-i", "--interactive", action="store_true", help="Attach interactive input (REPL) to bus")
    parser.add_argument(
        "-a",
        "--attach",
        help=(
            "Attach to bus via <channel>@<bitrate>. "
            "Backend defaults to socketcan (Linux) or gs_usb (macOS). "
            "Override with socketcan:<channel>, gs_usb:<index>, or toucan:<index>."
        ),
    )
    parser.add_argument("-c", "--configure", action="append", default=[], help="Configure ECU property <name>.<property>=<value>")
    parser.add_argument("--status-json", action="store_true", help="Emit JSON status snapshots")
    parser.add_argument("--status-interval", type=float, default=1.0, help="Status snapshot interval in seconds")
    parser.add_argument("--log", action="store_true", help="Enable semantic diagnostic logging with colors")
    parser.add_argument("--log-theme", choices=["light", "dark"], default="light", help="Color theme for log output (default: light)")
    parser.add_argument("--log-bytes-limit", type=int, default=20, help="Maximum bytes to show in hex output (default: 20)")
    parser.add_argument("--log-unlimited", action="store_true", help="Show unlimited hex bytes (overrides --log-bytes-limit)")
    parser.add_argument("--ecuconnect", action="store_true", help="Expose ECUconnect-compatible TCP adapter")
    parser.add_argument("--ecuconnect-host", default="0.0.0.0", help="ECUconnect TCP host (default: 0.0.0.0)")
    parser.add_argument("--ecuconnect-port", type=int, default=8129, help="ECUconnect TCP port (default: 8129)")
    parser.add_argument("--logger", action="store_true", help="Expose ECUconnect logger TCP stream")
    parser.add_argument("--logger-host", default="0.0.0.0", help="ECUconnect logger TCP host (default: 0.0.0.0)")
    parser.add_argument("--logger-name", default="", help="Custom Zeroconf name for ECUconnect logger")
    parser.add_argument("--hsfz", action="store_true", help="Expose BMW HSFZ-compatible TCP adapter")
    parser.add_argument("--hsfz-host", default="0.0.0.0", help="HSFZ TCP host (default: 0.0.0.0)")
    parser.add_argument("--hsfz-port", type=int, default=6801, help="HSFZ TCP port (default: 6801)")
    parser.add_argument("--hsfz-vin", default="", help="VIN used in HSFZ UDP vehicle announcement (default: first ECU vin or placeholder)")
    parser.add_argument("--doip", action="store_true", help="Expose DoIP (ISO 13400) TCP adapter")
    parser.add_argument("--doip-host", default="0.0.0.0", help="DoIP TCP host (default: 0.0.0.0)")
    parser.add_argument("--doip-port", type=int, default=13400, help="DoIP TCP port (default: 13400)")
    parser.add_argument("--doip-vin", default="", help="VIN used in DoIP UDP vehicle announcement (default: first ECU vin or placeholder)")
    parser.add_argument(
        "--doip-tester-address",
        type=_parse_tester_address,
        default=None,
        help="Restrict routing activation to a tester address (hex or decimal). Default: accept any.",
    )
    parser.add_argument("--elm327", action="store_true", help="Expose ELM327/STN2xxx TCP adapter")
    parser.add_argument("--elm327-host", default="0.0.0.0", help="ELM327/STN2xxx TCP host (default: 0.0.0.0)")
    parser.add_argument("--elm327-port", type=int, default=35000, help="ELM327/STN2xxx TCP port (default: 35000)")
    parser.add_argument("--elm327-name", default="ELM327/STN2xxx", help="Adapter identification name (STDI)")
    parser.add_argument("--elm327-startup-file", default="", help="Inject a file that contains adapter startup commands")
    parser.add_argument("--enet-service-broker", action="store_true", help="Expose ENET Service Broker vehicle bridge")
    parser.add_argument(
        "--enet-service-broker-url",
        default="",
        help="ENET Service Broker base URL override (default: empty = Zeroconf auto-discovery)",
    )
    parser.add_argument(
        "--enet-service-broker-mode",
        choices=["raw", "isotp"],
        default="isotp",
        help="ENET Service Broker transport mode (default: isotp)",
    )
    parser.add_argument(
        "--enet-service-broker-vin",
        default="",
        help="VIN used for ENET Service Broker session allocation (default: first ECU vin)",
    )
    parser.add_argument(
        "--bus-timing-factor",
        type=float,
        default=0.0,
        help="Artificial bus timing factor for virtual interfaces (0.0 = no delay, 1.0 = approximate CAN timing).",
    )
    parser.add_argument(
        "--isotp-fc-wait-count",
        type=int,
        default=0,
        help="Send N ISOTP flow-control WAIT frames before CTS (default: 0). Example: 2 for a short busy burst.",
    )
    parser.add_argument(
        "--isotp-fc-wait-ms",
        type=float,
        default=50.0,
        help="Delay in ms between injected ISOTP WAIT/CTS frames (default: 50). Example: 20-50 for a short stall.",
    )
    parser.add_argument(
        "--isotp-fc-wait-prob",
        type=float,
        default=0.0,
        help="Probability (0-1) of injecting an ISOTP WAIT sequence (default: 0). Example: 0.1 for 10 percent of FCs.",
    )
    parser.add_argument(
        "--isotp-fc-overflow-prob",
        type=float,
        default=0.0,
        help="Probability (0-1) of injecting an ISOTP OVERFLOW frame (default: 0). Example: 0.01 for 1 percent of FCs.",
    )
    parser.add_argument(
        "--isotp-fc-rng-seed",
        type=int,
        default=None,
        help="Seed for ISOTP flow-control randomness (default: unset). Example: 42 for repeatable tests.",
    )
    parser.add_argument(
        "--isotp-fc-overflow",
        action="store_true",
        help="Force ISOTP flow-control OVERFLOW frames instead of CTS. Example: simulate immediate ECU overrun.",
    )
    args = parser.parse_args(argv)

    _ensure_optional_dependencies(args)

    if (
        not args.interactive
        and not args.attach
        and not args.ecuconnect
        and not args.logger
        and not args.hsfz
        and not args.doip
        and not args.elm327
        and not args.enet_service_broker
    ):
        sys.exit("Error: Nothing to attach to the bus selected.")
    if args.interactive and args.attach:
        sys.exit("Error: Specify either a listener or an interactive input.")

    setup = Setup()
    for spec in args.specs:
        setup.add(spec)
    for config in args.configure:
        setup.add_configuration(config)

    wires = None
    bitrate = 500000
    if args.attach:
        if "@" not in args.attach:
            sys.exit("Error with attach specification: Expecting format <channel>@<bitrate>")
        interface, bitrate_str = args.attach.split("@", 1)
        bitrate = int(bitrate_str) if bitrate_str.isdigit() else 500000
        try:
            wires = SocketCANInterface(interface)
        except UnsupportedConfiguration as exc:
            sys.exit(f"Error with attach specification: {exc}")
    elif args.interactive:
        wires = REPLInterface()
    else:
        from .core.can import NullInterface
        wires = NullInterface()

    bus = CANBusEmulation(wires, timing_factor=args.bus_timing_factor)
    setup.enable_logging = args.log
    
    # Set color theme and hex limits if logging is enabled
    if args.log:
        from .core.diagnostic_interpreter import set_color_theme, set_hex_output_limit
        set_color_theme(args.log_theme)
        set_hex_output_limit(max_bytes=args.log_bytes_limit, unlimited=args.log_unlimited)
    
    setup.attach(bus)
    if (
        args.isotp_fc_wait_count > 0
        or args.isotp_fc_overflow
        or args.isotp_fc_wait_prob > 0
        or args.isotp_fc_overflow_prob > 0
    ):
        for ecu in setup._ecu_map.values():
            ecu.configure_isotp_flow_control(
                wait_count=args.isotp_fc_wait_count,
                wait_ms=args.isotp_fc_wait_ms,
                overflow=args.isotp_fc_overflow,
                wait_probability=args.isotp_fc_wait_prob,
                overflow_probability=args.isotp_fc_overflow_prob,
                seed=args.isotp_fc_rng_seed,
            )
    bus.operate(bitrate)

    ecuconnect_server = None
    ecuconnect_logger_server = None
    hsfz_server = None
    doip_server = None
    elm_server = None
    enet_service_broker_server = None
    if args.ecuconnect:
        from .ecuconnect import server as ecuconnect_server_module
        if ecuconnect_server_module.lz4_block is None:
            sys.exit("Error: ECUconnect requires lz4 compression support. Install with `pip install lz4`.")
        from .ecuconnect.server import ECUconnectServer
        ecuconnect_server = ECUconnectServer(bus, host=args.ecuconnect_host, port=args.ecuconnect_port, log=args.log)
        ecuconnect_server.start()
    if args.logger:
        from .ecuconnect.logger import ECUconnectLoggerServer
        service_name = args.logger_name.strip() or None
        ecuconnect_logger_server = ECUconnectLoggerServer(
            bus,
            host=args.logger_host,
            log=args.log,
            service_name=service_name,
        )
        if not ecuconnect_logger_server.start():
            sys.exit("Error: ECUconnect logger failed to start.")
    if args.hsfz:
        from .hsfz.server import HSFZServer
        vin = args.hsfz_vin.strip() if args.hsfz_vin else ""
        if not vin:
            vin = _resolve_vin_from_specs(setup._path_map) or ""
        hsfz_server = HSFZServer(
            bus,
            host=args.hsfz_host,
            port=args.hsfz_port,
            log=args.log,
            vin=vin or "WBA45678901234567",
        )
        hsfz_server.start()
    if args.doip:
        from .doip.server import DoIPServer
        vin = args.doip_vin.strip() if args.doip_vin else ""
        if not vin:
            vin = _resolve_vin_from_specs(setup._path_map) or ""
        doip_server = DoIPServer(
            bus,
            host=args.doip_host,
            port=args.doip_port,
            log=args.log,
            vin=vin or "WBA45678901234567",
            tester_address=args.doip_tester_address,
        )
        doip_server.start()
    if args.elm327:
        from elmpythonator.service import ELMService
        elm_server = ELMService(
            bus,
            host=args.elm327_host,
            port=args.elm327_port,
            adapter_name=args.elm327_name,
            startup_file=args.elm327_startup_file,
        )
        elm_server.start()
    if args.enet_service_broker:
        from .enet_service_broker.server import ENETServiceBrokerService

        vin = args.enet_service_broker_vin.strip() if args.enet_service_broker_vin else ""
        if not vin:
            vin = _resolve_vin_from_specs(setup._path_map) or ""
        if not vin:
            sys.exit("Error: ENET Service Broker requires a VIN (--enet-service-broker-vin or ECU/vehicle VIN).")
        enet_service_broker_server = ENETServiceBrokerService(
            bus,
            vin=vin,
            broker_url=args.enet_service_broker_url,
            mode=args.enet_service_broker_mode,
            log=args.log,
        )
        enet_service_broker_server.start()
    
    # Print startup message with ASCII table
    print(f"\n{'='*60}")
    if args.attach:
        attached_backend = getattr(wires, "backend", "socketcan")
        attached_channel = getattr(wires, "channel", "")
        print(f"CAN bus operating at {bitrate} bps via {attached_backend} ({attached_channel})")
    else:
        print(f"CAN bus: no physical interface attached (virtual {bitrate} bps)")
    if ecuconnect_server:
        print(f"ECUconnect: enabled on {args.ecuconnect_host}:{args.ecuconnect_port}")
    else:
        print("ECUconnect: disabled")
    if ecuconnect_logger_server:
        print(f"ECUconnect Logger: enabled on {args.logger_host}:{ecuconnect_logger_server.port}")
    else:
        print("ECUconnect Logger: disabled")
    if hsfz_server:
        print(f"HSFZ: enabled on {args.hsfz_host}:{args.hsfz_port}")
    else:
        print("HSFZ: disabled")
    if doip_server:
        tester_suffix = (
            f" [tester 0x{args.doip_tester_address:04X}]" if args.doip_tester_address is not None else ""
        )
        print(f"DoIP: enabled on {args.doip_host}:{args.doip_port}{tester_suffix}")
    else:
        print("DoIP: disabled")
    if elm_server:
        print(f"ELM327/STN2xxx: enabled on {args.elm327_host}:{args.elm327_port}")
    else:
        print("ELM327/STN2xxx: disabled")
    if enet_service_broker_server:
        if args.enet_service_broker_url.strip():
            print(
                f"ENET Service Broker: enabled via {args.enet_service_broker_url.strip()} "
                f"[{args.enet_service_broker_mode}]"
            )
        else:
            print(
                "ENET Service Broker: enabled via Zeroconf "
                "(enet-service-broker._enetbroker._tcp) "
                f"[{args.enet_service_broker_mode}]"
            )
    else:
        print("ENET Service Broker: disabled")
    print(f"{'='*60}")
    if not args.specs:
        print("Warning: No ECU specs loaded (virtual bus only).")
    print(f"\nLoaded ECUs:")
    print(format_arbitration_table(setup._ecu_map, setup._path_map))
    print(f"\n{setup.describe_loaded_configs()}")
    print(f"{'='*60}\n")

    services = []
    if ecuconnect_server:
        services.append(("ECUconnect", ecuconnect_server))
    if ecuconnect_logger_server:
        services.append(("ECUconnect Logger", ecuconnect_logger_server))
    if hsfz_server:
        services.append(("HSFZ", hsfz_server))
    if doip_server:
        services.append(("DoIP", doip_server))
    if elm_server:
        services.append(("ELM327/STN2xxx", elm_server))
    if enet_service_broker_server:
        services.append(("ENET Service Broker", enet_service_broker_server))

    # Set up signal handlers for graceful shutdown
    shutdown_requested = False
    
    def signal_handler(signum, frame):
        nonlocal shutdown_requested
        if shutdown_requested:
            # Force exit if Ctrl+C pressed twice
            print("\nForce shutdown...")
            sys.exit(1)
        shutdown_requested = True
        print("\nShutdown requested... Press Ctrl+C again to force quit.")
        bus.is_operating = False
    
    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)
    
    hub = None
    if args.status_json:
        hub = StatusHub(interval=args.status_interval)
        hub.add_sink(lambda snapshot: print(json.dumps(asdict(snapshot))))
        hub.start(bus, services=services)

    try:
        while bus.is_operating:
            time.sleep(0.1)
    except KeyboardInterrupt:
        # This shouldn't happen due to signal handler, but just in case
        pass
    finally:
        print("Shutting down ECUmulator...")
        
        # Stop status hub if running
        if hub:
            hub.stop()
        
        # Shutdown the CAN bus properly
        bus.shutdown()

        if ecuconnect_server:
            ecuconnect_server.stop()
        if ecuconnect_logger_server:
            ecuconnect_logger_server.stop()
        if hsfz_server:
            hsfz_server.stop()
        if doip_server:
            doip_server.stop()
        if elm_server:
            elm_server.stop()
        if enet_service_broker_server:
            enet_service_broker_server.stop()
            
        print("ECUmulator shutdown complete.")
