# # device_manager.py

# import adi
# # import uhd
# # from hackrf import *
# from ..common.utils import *
# import subprocess

# import time
# import gc

# #* Device manager *
# '''
# Responsible for starting and managing queues for devices.
# '''

# # TODO: Implement multiple device support
# #region PlutoSDR

# def connect_pluto(*, ip='', usb=''):
#     try:
#         if ip == '':
#             device = adi.Pluto(f'usb:{usb}')
#             print(f"Connected to Pluto usb:{usb}")
#         else:
#             device = adi.Pluto(f'ip:{ip}')
#             print(f"Connected to Pluto ip:{ip}")
#         return device
#     except Exception as e:
#         print(f"Pluto {ip}: {e}")
#         return None

# import subprocess
# import re

# def get_usb_port_from_serial(serial):
#     try:
#         # Run iio_info -s and get the output
#         output = subprocess.check_output(["iio_info", "-s"]).decode("utf-8")
#     except Exception as e:
#         print(f"Error running iio_info: {e}")
#         return None

#     # Regex to find the USB port associated with the given serial number
#     pattern = re.compile(rf"\d+:.*serial={serial}\s+\[usb:(\S+)\]")

#     for line in output.splitlines():
#         match = pattern.search(line)
#         if match:
#             return match.group(1)  # Return the USB port

#     print(f"No device found with serial {serial}")
#     return None

# # def connect_hackrf(device_index):
# #     try:
# #         device = HackRF(device_index=device_index)
# #         print(f"Connected to HackRF {device_index}")
# #         return device
# #     except Exception as e:
# #         print(f"HackRF {device_index}: {e}")
# #         return None
    
# # def connect_usrp2901():
# #     usrp = uhd.usrp.MultiUSRP()
# #     print(f"Connected to USRP 2901")
# #     return usrp

# device_serialization = {
#     0: '10447392da11001009001500f02af8a678',
#     1: '10447392da110010f9ff1500612ff8ecb0',
#     # 2: '104473b04a06000ffbff3900f5de83267e', # GONE?
#     2: '10447318ac0f0003050036008365171a2d',
#     3: '10447376de0b00130b00200023119a89c6',
#     4: '1044732a98110002faff1500f92ae517bf',
    
#     5: '10447318ac0f001915003200073317bbca',
#     6: '104473e6a60f00020d003900e21716bf22',
#     7: '104473e6a60f000cf3ff17000b3917d48f',
#     8: '10447318ac0f0016faff2500998717eacf',
# }

# master_token = 'SuperCoolTokenForIan'

# # print("New Version")

# devices = { # device, salt, hash
    
#     # iio_info -s
#     # ^ find usb serial ports
    
#     # 0: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[0])),'',''),
#     # 1: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[1])),'',''),
#     # 2: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[2])),'',''),
#     # 3: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[3])),'',''),
#     # 4: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[4])),'',''),
    
    
#     # 5: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[5])),'',''),
#     # 6: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[6])),'',''),
#     # 7: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[7])),'',''),
#     # 8: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[8])),'',''),
    
#     # 0: (connect_pluto(usb='1.8.5'),'',''),
#     # 1: (connect_pluto(usb='1.18.5'),'',''),
#     # 2: (connect_pluto(usb='1.8.5'),'',''),
#     # 1: (connect_pluto(usb='1.8.5'),'',''),
    
#     # hackrf
#     # 10: (connect_hackrf(0),'',''),
    
#     # 20: (connect_usrp2901(),'',''),
    
# }

# devices_info = {    # device, info
#     0: "Pluto SDR (Loopback)",
#     1: "Pluto SDR (Loopback)",
#     2: "Pluto SDR (OTA + BPF)",
#     3: "Pluto SDR (OTA + BPF)",
#     4: "Pluto SDR (OTA + BPF)",
#     5: "Pluto SDR (OTA)",
#     6: "Pluto SDR (OTA)",
#     7: "Pluto SDR (OTA)",
#     8: "Pluto SDR (OTA)",
# }

# # transmitter = subprocess.Popen(['python3', 'transmitter.py'])
# # transmitter.terminate()

# def start_transmitter():
#     pass
#     # global transmitter
#     # transmitter = subprocess.Popen(['python3', 'transmitter.py'])
    
# def terminate_transmitter():
#     pass
#     # global transmitter
#     # transmitter.terminate()
    
# def get_transmitter_state():
#     global transmitter
#     return transmitter.poll() == None

# def get_device(device_id):
#     return devices[device_id]

# def get_all_devices() -> dict:
#     return {k: v for k, v in devices.items() if v is not None}

# def get_all_devices_str() -> dict:
#     return devices_info

# def parse_mastertoken(token: str):
#     """
#     Matches:
#       {master_token}_0
#       {master_token}-1
#       {master_token}_0_force

#     Returns (device_id:int, force:bool) or None.
#     """
#     if not token:
#         return None

#     prefix = re.escape(master_token)
#     pattern = re.compile(rf"^{prefix}[_-](\d+)(?:_force)?$")

#     m = pattern.match(token)
#     if not m:
#         return None

#     device_id = int(m.group(1))
#     force = token.endswith("_force")
#     return (device_id, force)
# def device_exists(device_id: int) -> bool:
#     if device_id not in devices:
#         return False
#     dev, _, _ = devices[device_id]
#     return dev is not None

# def device_is_available(device_id: int) -> bool:
#     if device_id not in devices:
#         return False
#     dev, salt, hsh = devices[device_id]
#     return (dev is not None) and (salt == '') and (hsh == '')

# def get_device_by_id(device_id: int):
#     return devices[device_id]

# def get_device(*, api_token):
    
#     if api_token == master_token:
#         # Check for the first empty device
#         for key, value in devices.items():
#             if value[1] == '' and value[2] == '':
#                 return value[0]
            
#         return None
    
#     parsed = parse_mastertoken(api_token) # Special args for specific devices
#     if parsed:
#         device_id, force = parsed

#         if not device_exists(device_id):
#             return None

#         if force:
#             # Force access if the device exists (ignores reservation state)
#             return devices[device_id][0]

#         # Non-force: only if device exists AND is not reserved
#         if device_is_available(device_id):
#             return devices[device_id][0]

#         return None
    
#     # Regular Behavior
#     for key, value in devices.items():
#         if validate_token(value[1], value[2], api_token):
#             return value[0]
#     return None
    
# def set_device(device_id, salt, hash):
#     devices[device_id] = (devices[device_id][0], salt, hash)
    
# def set_pluto(ip='192.168.2.1'):
#     # try:
#     #     global device_pluto
#     #     del device_pluto
#     #     device_pluto = adi.Pluto(f'ip:{ip}')
#     # except Exception as e:
#     #     print(f"PlutoError: {e}")
#     # NO IMPLEMENTATION SHOUlD BE GIVEN, AS THIS IS FOR DEVICE SETUP. Keeping it just to support common Pluto code
#     pass
# device_manager.py

from __future__ import annotations

import os
import re
import subprocess
import threading
from pathlib import Path
from typing import Dict, Optional, Tuple

import adi

from ..common.utils import validate_token  # token hash/salt validator from your utils


# * Device manager *
"""
Device config (list/names/identifiers) is loaded from a .env-like file:
  ~/.config/remoterf/devices.env
  (override with REMOTERF_CONFIG_DIR)

Reservation/auth state (per-device salt/hash) is kept separately in-memory,
but exposed through the same legacy functions:
  - get_all_devices()
  - get_all_devices_str()
  - set_device(device_id, salt, hash)
  - get_device(api_token=...)
  - device_exists(), device_is_available(), get_device_by_id()

Expected keys per device (hostrf-compatible):
  DEVICE_<gid>_TYPE=pluto
  DEVICE_<gid>_NAME=pluto_aaa
  DEVICE_<gid>_IDENT_KIND=iio_serial | usb | ip
  DEVICE_<gid>_IDENT=<serial_or_usb_or_ip>

Back-compat:
  DEVICE_<gid>_ID=<serial>  # treated as IDENT_KIND=iio_serial, TYPE=pluto, NAME=device-<gid>
"""


# -----------------------------
# Config paths (server-side)
# -----------------------------

def _xdg_config_home() -> Path:
    return Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))

def _cfg_dir() -> Path:
    # default: ~/.config/remoterf
    return Path(os.getenv("REMOTERF_CONFIG_DIR", _xdg_config_home() / "remoterf"))

def _devices_env_path() -> Path:
    return _cfg_dir() / "devices.env"


# -----------------------------
# Pluto helpers
# -----------------------------

def connect_pluto(*, ip: str = "", usb: str = ""):
    try:
        if ip == "":
            dev = adi.Pluto(f"usb:{usb}")
            print(f"Connected to Pluto usb:{usb}")
        else:
            dev = adi.Pluto(f"ip:{ip}")
            print(f"Connected to Pluto ip:{ip}")
        return dev
    except Exception as e:
        print(f"Pluto {ip}: {e}")
        return None

def get_usb_port_from_serial(serial: str) -> Optional[str]:
    try:
        output = subprocess.check_output(["iio_info", "-s"]).decode("utf-8", errors="replace")
    except Exception as e:
        print(f"Error running iio_info: {e}")
        return None

    # Example line: "... serial=<SERIAL> [usb:1.2.3]"
    pattern = re.compile(rf"\d+:.*serial={re.escape(str(serial))}\s+\[usb:(\S+)\]")

    for line in output.splitlines():
        m = pattern.search(line)
        if m:
            return m.group(1)

    print(f"No device found with serial {serial}")
    return None


# -----------------------------
# Parse devices.env
# -----------------------------

_ENV_LINE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$")
_DEVICE_KEY_RE = re.compile(r"^DEVICE_(\d+)_(.+)$", re.IGNORECASE)

def _strip_quotes(v: str) -> str:
    v = (v or "").strip()
    if len(v) >= 2 and ((v[0] == v[-1] == '"') or (v[0] == v[-1] == "'")):
        return v[1:-1]
    return v

def _read_env_file(path: Path) -> Dict[str, str]:
    out: Dict[str, str] = {}
    if not path.exists():
        return out
    try:
        txt = path.read_text(encoding="utf-8")
    except Exception:
        return out

    for raw in txt.splitlines():
        line = raw.strip()
        if not line or line.startswith("#"):
            continue
        m = _ENV_LINE.match(line)
        if not m:
            continue
        k = m.group(1).strip()
        v = _strip_quotes(m.group(2))
        out[k] = v
    return out

def _load_device_records() -> Dict[int, Dict[str, str]]:
    """
    Returns:
      { gid: { "TYPE":..., "NAME":..., "IDENT_KIND":..., "IDENT":..., ... } }
    """
    kv = _read_env_file(_devices_env_path())
    recs: Dict[int, Dict[str, str]] = {}

    # New format: DEVICE_<gid>_<FIELD>
    for k, v in kv.items():
        m = _DEVICE_KEY_RE.match(k.strip())
        if not m:
            continue
        try:
            gid = int(m.group(1))
        except Exception:
            continue
        field = m.group(2).strip().upper()
        recs.setdefault(gid, {})[field] = str(v).strip()

    # Back-compat: DEVICE_<gid>_ID treated as IDENT
    for k, v in kv.items():
        kk = k.strip().upper()
        if not kk.startswith("DEVICE_") or not kk.endswith("_ID"):
            continue
        mid = kk[7:-3]
        try:
            gid = int(mid)
        except Exception:
            continue
        recs.setdefault(gid, {})
        recs[gid].setdefault("TYPE", "pluto")
        recs[gid].setdefault("NAME", f"device-{gid}")
        recs[gid].setdefault("IDENT_KIND", "iio_serial")
        recs[gid].setdefault("IDENT", str(v).strip())

    return recs

def _connect_from_record(rec: Dict[str, str]):
    """
    Returns (device_obj, ident, dtype).
    """
    dtype = (rec.get("TYPE") or "").strip().lower()
    ident_kind = (rec.get("IDENT_KIND") or "").strip().lower()
    ident = (rec.get("IDENT") or "").strip()

    if dtype != "pluto":
        return (None, ident, dtype)

    if ident_kind in ("iio_serial", "serial", "iio"):
        if not ident:
            return (None, ident, dtype)
        usb_port = get_usb_port_from_serial(ident)
        if not usb_port:
            return (None, ident, dtype)
        return (connect_pluto(usb=usb_port), ident, dtype)

    if ident_kind == "usb":
        if not ident:
            return (None, ident, dtype)
        return (connect_pluto(usb=ident), ident, dtype)

    if ident_kind == "ip":
        if not ident:
            return (None, ident, dtype)
        return (connect_pluto(ip=ident), ident, dtype)

    return (None, ident, dtype)


# -----------------------------
# Runtime state
#   - devices_info: gid -> display name
#   - device_serialization: gid -> IDENT (for debug)
#   - devices: gid -> (device_obj, salt, hash)   [LEGACY SHAPE]
#   - _meta: gid -> (ident, dtype)               [optional]
# -----------------------------

_lock = threading.RLock()

_records = _load_device_records()

devices_info: Dict[int, str] = {}
device_serialization: Dict[int, str] = {}
_meta: Dict[int, Tuple[str, str]] = {}

# legacy: gid -> (dev, salt, hash)
devices: Dict[int, Tuple[object, str, str]] = {}

for gid, rec in sorted(_records.items()):
    name = (rec.get("NAME") or f"device-{gid}").strip()
    ident = (rec.get("IDENT") or "").strip()
    dtype = (rec.get("TYPE") or "").strip().lower()

    devices_info[int(gid)] = name
    if ident:
        device_serialization[int(gid)] = ident

    dev, ident2, dtype2 = _connect_from_record(rec)
    _meta[int(gid)] = (ident2, dtype2)

    # legacy tuple, reservation state starts empty
    devices[int(gid)] = (dev, "", "")


# -----------------------------
# Tokens / master token support (legacy)
# -----------------------------

master_token = os.getenv("REMOTERF_MASTER_TOKEN", "SuperCoolTokenForIan")

def parse_mastertoken(token: str):
    """
    Matches:
      {master_token}_0
      {master_token}-1
      {master_token}_0_force

    Returns (device_id:int, force:bool) or None.
    """
    if not token:
        return None
    prefix = re.escape(master_token)
    pattern = re.compile(rf"^{prefix}[_-](\d+)(?:_force)?$")
    m = pattern.match(token)
    if not m:
        return None
    device_id = int(m.group(1))
    force = token.endswith("_force")
    return (device_id, force)


# -----------------------------
# Transmitter stubs (legacy)
# -----------------------------

_transmitter = None

def start_transmitter():
    # keep no-op unless you wire it back in
    # global _transmitter
    # _transmitter = subprocess.Popen(["python3", "transmitter.py"])
    pass

def terminate_transmitter():
    # global _transmitter
    # if _transmitter is not None:
    #     _transmitter.terminate()
    pass

def get_transmitter_state() -> bool:
    # global _transmitter
    # return (_transmitter is not None) and (_transmitter.poll() is None)
    return False


# -----------------------------
# Legacy API expected by server + reservation handler
# -----------------------------

def get_all_devices() -> Dict[int, Tuple[object, str, str]]:
    # Only return entries with a connected device object
    with _lock:
        return {k: v for k, v in devices.items() if v is not None and v[0] is not None}

def get_all_devices_str() -> Dict[int, str]:
    with _lock:
        return dict(devices_info)

def set_device(device_id: int, salt: str, hash: str):
    """
    Update ONLY reservation/auth state (salt/hash) while preserving the device object.
    Keeps the legacy devices tuple shape: (dev, salt, hash).
    """
    with _lock:
        if device_id not in devices:
            return
        dev, _, _ = devices[device_id]
        devices[device_id] = (dev, str(salt), str(hash))

def device_exists(device_id: int) -> bool:
    with _lock:
        if device_id not in devices:
            return False
        dev, _, _ = devices[device_id]
        return dev is not None

def device_is_available(device_id: int) -> bool:
    with _lock:
        if device_id not in devices:
            return False
        dev, salt, hsh = devices[device_id]
        return (dev is not None) and (salt == "") and (hsh == "")

def get_device_by_id(device_id: int):
    """
    Legacy helper: returns (dev, salt, hash) tuple.
    """
    with _lock:
        return devices.get(device_id)

def get_device_obj_by_id(device_id: int):
    """
    Convenience: returns just the device object.
    """
    with _lock:
        t = devices.get(device_id)
        return t[0] if t else None

def get_device(*, api_token: str):
    """
    Legacy behavior:
      - master_token returns first available device
      - master_token_<id>[_force] selects a specific device
      - otherwise validate against per-device salt/hash (set via reservations)
    Returns the connected device object or None.
    """
    if not api_token:
        return None

    with _lock:
        if api_token == master_token:
            # first available device
            for _, (dev, salt, hsh) in devices.items():
                if dev is not None and salt == "" and hsh == "":
                    return dev
            return None

        parsed = parse_mastertoken(api_token)
        if parsed:
            device_id, force = parsed

            if device_id not in devices:
                return None

            dev, salt, hsh = devices[device_id]
            if dev is None:
                return None

            if force:
                return dev

            # non-force: only if unreserved
            if salt == "" and hsh == "":
                return dev
            return None

        # Regular token validation (against per-device salt/hash state)
        for _, (dev, salt, hsh) in devices.items():
            if dev is None:
                continue
            if salt and hsh and validate_token(salt, hsh, api_token):
                return dev

        return None

def reload_devices() -> None:
    """
    Optional utility: reload devices.env and reconnect devices.
    Preserves existing salt/hash for devices that remain.
    """
    global _records, devices_info, device_serialization, _meta, devices

    with _lock:
        old_state: Dict[int, Tuple[str, str]] = {}
        for gid, tup in devices.items():
            try:
                old_state[int(gid)] = (tup[1], tup[2])
            except Exception:
                pass

        _records = _load_device_records()

        new_devices_info: Dict[int, str] = {}
        new_device_serialization: Dict[int, str] = {}
        new_meta: Dict[int, Tuple[str, str]] = {}
        new_devices: Dict[int, Tuple[object, str, str]] = {}

        for gid, rec in sorted(_records.items()):
            name = (rec.get("NAME") or f"device-{gid}").strip()
            ident = (rec.get("IDENT") or "").strip()

            new_devices_info[int(gid)] = name
            if ident:
                new_device_serialization[int(gid)] = ident

            dev, ident2, dtype2 = _connect_from_record(rec)
            new_meta[int(gid)] = (ident2, dtype2)

            salt, hsh = old_state.get(int(gid), ("", ""))
            new_devices[int(gid)] = (dev, salt, hsh)

        devices_info = new_devices_info
        device_serialization = new_device_serialization
        _meta = new_meta
        devices = new_devices

def set_pluto(ip: str = "192.168.2.1"):
    # Intentionally no-op for your setup; kept for compatibility.
    pass
