import datetime
import json
import os
import subprocess
import sys
import time

import typer
from pyfzf.pyfzf import FzfPrompt

from celilo.common import saru_home
from celilo.reporting.PayloadTraceReport import PayloadTraceReport
from celilo.reporting.StreamReportGenerator import StreamReportGenerator
from celilo.reporting.message_transmission_report import MessageTransmissionReport
from celilo.reporting.test_controller import StreamTestController
from celilo.usb_node_detector import USBNodeDetector
from celilo.firmware_updater import FirmwareUpdater
from celilo.Adb import Adb
# from celilo.workspace_selector import select_workspace

ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))

sys.path.append(ROOT_DIR)
app = typer.Typer()

print(f"working directory: {saru_home}")

# Configuration directory and file path
CONFIG_DIR = os.path.expanduser("~/.somewear/.gridpilot")
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")


def load_config():
    """Load configuration from file."""
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE, 'r') as f:
            return json.load(f)
    return {}


def save_config(config):
    """Save configuration to file."""
    # Create config directory if it doesn't exist
    os.makedirs(CONFIG_DIR, exist_ok=True)
    with open(CONFIG_FILE, 'w') as f:
        json.dump(config, f, indent=2)


@app.command(name="set-org-id", help="Set the Somewear organization ID for grid_pilot.")
def set_org_id(
        org_id: str = typer.Argument(help="Organization ID to save"),
        org_name: str = typer.Argument(help="Organization name to save")
):
    """
    Set and save the organization ID and name to the configuration file.

    Args:
        org_id: The organization ID to save.
        org_name: The organization name to save.
    """
    config = load_config()
    config['org_id'] = org_id
    config['org_name'] = org_name
    save_config(config)
    typer.echo(f"✓ Organization ID set to: {org_id}")
    typer.echo(f"✓ Organization name set to: {org_name}")
    typer.echo(f"  Saved to: {CONFIG_FILE}")


@app.command(name="get-org-id", help="Get the saved organization ID.")
def get_org_id():
    """
    Retrieve and display the saved organization ID and name from the configuration file.
    """
    config = load_config()
    org_id = config.get('org_id')
    org_name = config.get('org_name')

    if org_id:
        typer.echo(f"Organization ID: {org_id}")
        typer.echo(f"Organization Name: {org_name if org_name else '(not set)'}")
    else:
        typer.echo("No organization ID has been set.")
        typer.echo(f"Use 'gp set-org-id <id> <name>' to set one.")
        raise typer.Exit(code=1)


@app.command(name="get-workspace", help="Get the saved workspace information.")
def get_workspace():
    """
    Retrieve and display the saved workspace information from the configuration file.
    """
    config = load_config()
    workspace_id = config.get('workspace_id')
    workspace_name = config.get('workspace_name')

    if workspace_id:
        typer.echo(f"Workspace ID: {workspace_id}")
        typer.echo(f"Workspace Name: {workspace_name if workspace_name else '(not set)'}")
    else:
        typer.echo("No workspace has been selected.")
        typer.echo(f"Use 'gp select-workspace' to select one.")
        raise typer.Exit(code=1)


# Register the select-workspace command from workspace_selector module
# app.command(name="select-workspace", help="Select a workspace from the organization using fzf.")(select_workspace)


@app.command(name="launch-app", help="Launch the app with a URI deep link.")
def launch_app(
        uri: str = typer.Argument(help="URI to launch the app with (e.g., somewear://api.somewear.co:443/workspace-token?...)"),
        config_path: str = typer.Option(help="Config file path"),
        app_package: str = typer.Option(
            "com.somewearlabs.atak.debug",
            help="The package name of the app. Options: 'com.somewearlabs.atak.debug' or 'com.atakmap.app.civ'."
        )
):
    """
    Launch the app using ADB with a custom URI deep link.

    Args:
        uri: The URI to launch the app with (e.g., workspace-token URI)
        config_path: Path to config file with device settings
        app_package: The package name of the app to launch
    """
    # Validate config path exists
    if not os.path.exists(config_path):
        typer.echo(f"Error: Config file not found: {config_path}", err=True)
        raise typer.Exit(code=1)

    # Load config
    import json
    with open(config_path, 'r') as f:
        config = json.load(f)

    # Initialize ADB
    adb = Adb(app_package)

    # Get connected devices
    devices = adb.devices()

    if not devices or not devices[0]:
        typer.echo("Error: No ADB devices found.", err=True)
        typer.echo("Make sure a device is connected and ADB is enabled.", err=True)
        raise typer.Exit(code=1)

    typer.echo(f"Found {len(devices)} device(s): {', '.join(devices)}")

    # Determine host based on package
    is_atak = app_package == "com.atakmap.app.civ"
    if is_atak:
        host = "somewear"
    else:
        host = "atak_somewear_test"

    typer.echo(f"Launching {app_package} with URI...")

    # Launch on each device
    for device in devices:
        # Get device-specific config
        if device in config:
            msgInterval = config[device]["message_interval"]
            callsign = config[device].get("callsign", "")
            callsignParam = f"callsign={callsign}" if callsign else ""
        else:
            msgInterval = 0
            callsignParam = ""
            typer.echo(f"Warning: Device {device} not found in config file", err=True)

        # Parse the URI to modify or add parameters
        # If the URI already has query params, append our params; otherwise add them
        if '?' in uri:
            separator = '&'
        else:
            separator = '?'

        # Build the full URI with additional parameters
        if msgInterval or callsignParam:
            full_uri = f"{uri}{separator}msgInterval={msgInterval}"
            if callsignParam:
                full_uri += f"&{callsignParam}"
        else:
            full_uri = uri

        # Replace the scheme prefix if needed (handle both somewear:// and full URIs)
        if full_uri.startswith("somewear://") or full_uri.startswith("atak_somewear_test://"):
            # URI already has a scheme, keep it as-is
            launch_uri = full_uri
        elif full_uri.startswith("api.somewear.co"):
            # Add the appropriate scheme
            launch_uri = f"{host}://{full_uri}"
        else:
            # Assume it's a partial URI
            launch_uri = full_uri

        # Escape & characters for shell
        escaped_uri = launch_uri.replace('&', '\\&')

        # Build ADB command
        command = (
            f'adb -s {device} shell am start -a android.intent.action.VIEW -d '
            f'"{escaped_uri}" --ez collectAnalytics true'
        )

        typer.echo(f"\n→ Launching on device: {device}")
        typer.echo(f"  Command: {command}")

        # Execute command
        result = os.system(command)

        if result == 0:
            typer.echo(f"  ✓ Successfully launched on {device}")
        else:
            typer.echo(f"  ✗ Failed to launch on {device}", err=True)

    typer.echo(f"\n✓ Launch complete")


@app.command(name="stream_test", help="Runs a stream test for a specified duration.")
def stream_test(
        duration_minutes: float,
        config_path: str = typer.Option(help="Config file path"),
        app_package: str = typer.Option(
            "com.somewearlabs.atak.debug",
            help="The package name of the app to test. Options: 'com.somewearlabs.atak.debug' or 'com.atakmap.app.civ' (default)."
        ),
        workspace_id: str = typer.Option(
            None,
            help="The workspace ID to use for the test; must be a workspace that the client has already joined. If not provided, uses saved workspace ID or org ID."
        ),
        workspace_key: str = typer.Option(
            None,
            help="The workspace-key used to join a workspace"
        ),
        workspace_name: str = typer.Option(
            None,
            help="The workspace name used to join a workspace"
        )
):
    assert duration_minutes
    assert duration_minutes
    assert config_path

    # If workspace_id is not provided, try to load from config
    if workspace_id is None:
        config = load_config()
        # Prefer saved workspace_id from select-workspace, fall back to org_id
        workspace_id = config.get('workspace_id') or config.get('org_id')
        saved_workspace_name = config.get('workspace_name')
        org_name = config.get('org_name')

        if workspace_id:
            if saved_workspace_name:
                typer.echo(f"Using saved workspace: {saved_workspace_name} (ID: {workspace_id})")
            elif org_name:
                typer.echo(f"Using saved organization: {org_name} (ID: {workspace_id})")
            else:
                typer.echo(f"Using saved workspace ID: {workspace_id}")
        else:
            typer.echo("Error: No workspace ID provided and no saved workspace/org ID found.", err=True)
            typer.echo("Either provide --workspace-id, use 'gp select-workspace', or set org with 'gp set-org-id <id> <name>'", err=True)
            raise typer.Exit(code=1)

    if workspace_key:
        assert workspace_name, "Workspace name must be provided if workspace key is specified"

    # config_path exists
    if not os.path.exists(config_path):
        raise Exception(f"{config_path} does not exist")

    """  
        Executes a stream test for a given duration.  

        Args:  
            duration_minutes (float): The duration of the test in minutes.  
            app_package (str, optional): The package name of the app to test. Defaults to "com.atakmap.app.civ",
            but can also use com.somewearlabs.atak.debug".  
        """

    current_time = time.time()
    current_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(current_time))

    reports = StreamTestController(current_time, duration_minutes, app_package, workspace_id, config_path,
                                   workspace_key, workspace_name).start()

    payload_trace = PayloadTraceReport(current_time, duration_minutes, app_package, workspace_id, config_path)
    payload_trace.generate_report()

    return reports


@app.command(name="blank_command", help="")
def blank_command(duration_minutes: float):
    assert duration_minutes


@app.command(name="list_usb_nodes", help="List all Somewear nodes connected via USB.")
def list_usb_nodes():
    """
    Detect and list all Somewear nodes connected via USB.
    Excludes Android devices.
    """
    detector = USBNodeDetector()
    nodes = detector.detect_nodes()
    detector.print_nodes()

    if not nodes:
        typer.echo("No Somewear nodes detected via USB.")
        raise typer.Exit(code=1)


@app.command(name="update_firmware", help="Update firmware on a Somewear node using newtmgr.")
def update_firmware(
        firmware_path: str = typer.Argument(help="Path to the firmware binary file (e.g., app_update.bin)"),
        mtu: int = typer.Option(4096, help="MTU size for newtmgr connection")
):
    """
    Update firmware on a connected Somewear node using newtmgr.

    Args:
        firmware_path: Path to the firmware binary file
        mtu: MTU size for the serial connection (default: 4096)
    """
    # Initialize updater
    updater = FirmwareUpdater(mtu=mtu)

    # Get available nodes
    nodes = updater.get_available_nodes()

    if not nodes:
        typer.echo("No Somewear nodes detected via USB.", err=True)
        typer.echo("Please connect a Somewear node and try again.", err=True)
        raise typer.Exit(code=1)

    # Select node
    selected_node = None
    if len(nodes) == 1:
        selected_node = nodes[0]
        mode_str = " [BOOTLOADER]" if selected_node.bootloader_mode else ""
        typer.echo(f"Found 1 node: {selected_node.product_name} ({selected_node.serial_number}){mode_str}")
    else:
        # Multiple nodes - use fzf to select
        typer.echo(f"Found {len(nodes)} nodes. Please select one:")
        node_options = []
        for node in nodes:
            mode_str = " [BOOTLOADER]" if node.bootloader_mode else ""
            node_options.append(
                f"{node.product_name} ({node.serial_number}) @ {node.device_path}{mode_str}"
            )

        fzf = FzfPrompt()
        try:
            selected = fzf.prompt(node_options, '--prompt="Select node: "')
            if not selected:
                typer.echo("No node selected.")
                raise typer.Exit(code=0)

            # Find the selected node
            selected_display = selected[0]
            for node in nodes:
                if node.serial_number in selected_display:
                    selected_node = node
                    break
        except Exception as e:
            typer.echo(f"Error during selection: {e}", err=True)
            raise typer.Exit(code=1)

    if not selected_node:
        typer.echo("Error: Could not select a node.", err=True)
        raise typer.Exit(code=1)

    # Display selected node info
    mode_str = " [BOOTLOADER MODE]" if selected_node.bootloader_mode else ""
    typer.echo(f"\nSelected node:")
    typer.echo(f"  Product: {selected_node.product_name}{mode_str}")
    typer.echo(f"  Serial:  {selected_node.serial_number}")
    typer.echo(f"  Path:    {selected_node.device_path}")
    typer.echo(f"\nUploading firmware: {firmware_path}")

    # Perform update
    result = updater.update_firmware(firmware_path, selected_node)

    # Display results
    if result.upload_output:
        typer.echo("\nUpload output:")
        typer.echo(result.upload_output)

    if result.success:
        typer.echo("✓ Firmware uploaded successfully")

        if result.reset_output:
            typer.echo("\nReset output:")
            typer.echo(result.reset_output)
            typer.echo("✓ Node reset successfully")

        typer.echo("\n✓ Firmware update complete")
    else:
        typer.echo(f"\nError: {result.message}", err=True)
        raise typer.Exit(code=1)


if __name__ == '__main__':
    app()
