import subprocess
import ipaddress
import re

from .common import insert,YangDate
from .host import HOST

def uboot_get_boot_order():
    data = HOST.run_multiline("fw_printenv BOOT_ORDER".split(), [])
    for line in data:
        if "BOOT_ORDER" in line:
            return line.strip().split("=")[1].split()

    raise Exception

def grub_get_boot_order():
    data = HOST.run_multiline("grub-editenv /mnt/aux/grub/grubenv list".split(), [])
    for line in data:
        if "ORDER" in line:
            return line.split("=")[1].strip().split()

    raise Exception

def get_boot_order():
    order = None
    try:
        order = uboot_get_boot_order()
    except:
        pass
    try:
        if order is None:
            order = grub_get_boot_order()
    except:
        pass

    return order

def add_ntp(out):
    """Add NTP source information from chronyc sources.

    The chronyc -c sources output is CSV with the following fields:
      [0] Mode indicator:
          ^  server (we're a client to this source)
          =  peer (symmetric mode)
          #  local reference clock (GPS, PPS, etc.) - skipped, no IP address
      [1] State indicator:
          *  selected (current sync source)
          +  candidate
          -  outlier
          ?  unusable
          x  falseticker
          ~  unstable
      [2] Address (IP address or refclock name like "GPS")
      [3] Stratum
      [4] Poll interval (log2 seconds)
      [5] Reach (octal reachability register)
      [6] LastRx (seconds since last response)
      [7] Last offset (seconds)
      [8] Offset at last update (seconds)
      [9] Error estimate (seconds)
    """
    data = HOST.run_multiline(["chronyc", "-c", "sources"], [])
    source = []
    state_mode_map = {
        "^": "server",
        "=": "peer",
        "#": "local-clock"
    }
    source_state_map = {
        "*": "selected",
        "+": "candidate",
        "-": "outlier",
        "?": "unusable",
        "x": "falseticker",
        "~": "unstable"
    }
    for line in data:
        src = {}
        line = line.split(',')
        # Skip reference clocks (mode "#") as they have names like "GPS" instead of IP addresses
        if line[0] == "#":
            continue
        src["address"] = line[2]
        src["mode"] = state_mode_map[line[0]]
        src["state"] = source_state_map[line[1]]
        src["stratum"] = int(line[3])
        src["poll"] = int(line[4])
        source.append(src)

    insert(out, "infix-system:ntp", "sources", "source", source)


def add_dns(out):
    options = {}
    servers = []
    search = []

    content = HOST.read_multiline("/etc/resolv.conf.head", [])
    for line in content:
        line = line.strip()

        if line.startswith('nameserver'):
            ip = line.split()[1]
            try:
                ipaddress.ip_address(ip)
                servers.append({
                    "address": ip,
                    "origin": "static"
                })
            except ValueError:
                continue

        elif line.startswith('search'):
            search.extend(line.split()[1:])

        elif line.startswith('options'):
            opts = line.split()[1:]
            for opt in opts:
                if opt.startswith('timeout:'):
                    options["timeout"] = int(opt.split(':')[1])
                elif opt.startswith('attempts:'):
                    options["attempts"] = int(opt.split(':')[1])

    output = HOST.run_multiline(['/sbin/resolvconf', '-l'], [])
    for line in output:
        line = line.strip()
        if line.startswith('nameserver'):
            parts = line.split('#', 1)
            ip = parts[0].split()[1]

            iface = None
            if len(parts) > 1:
                iface = parts[1].strip()

            try:
                ipaddress.ip_address(ip)
                servers.append({
                    "address": ip,
                    "origin": "dhcp",
                    "interface": iface
                })
            except ValueError:
                continue

        elif line.startswith('search'):
            parts = line.split('#', 1)
            search.extend(parts[0].split()[1:])

    insert(out, "infix-system:dns-resolver", "options", options)
    insert(out, "infix-system:dns-resolver", "server", servers)
    insert(out, "infix-system:dns-resolver", "search", search)

def add_software_slots(out, data):
    slots = []
    for slot in data.get("slots", []):
        for key, value in slot.items():
            new = {}
            new["name"] = key
            new["bootname"] = slot[key].get("bootname")
            new["class"] = slot[key].get("class")
            new["state"] = slot[key].get("state")
            new["bundle"] = {}
            slot_status=value.get("slot_status", {})
            if slot_status.get("bundle", {}).get("compatible"):
                new["bundle"]["compatible"] = slot_status.get("bundle", {}).get("compatible")
            if slot_status.get("bundle", {}).get("version"):
                new["bundle"]["version"] = slot_status.get("bundle", {}).get("version")
            if slot_status.get("checksum", {}).get("size"):
                new["size"] = str(slot_status.get("checksum", {}).get("size"))
            if slot_status.get("checksum", {}).get("sha256"):
                new["sha256"] = slot_status.get("checksum", {}).get("sha256")

            new["installed"] = {}
            if slot_status.get("installed", {}).get("timestamp"):
                new["installed"]["datetime"] = slot_status.get("installed", {}).get("timestamp")

            if slot_status.get("installed", {}).get("count"):
                new["installed"]["count"] = slot_status.get("installed", {}).get("count")

            new["activated"] = {}
            if slot_status.get("activated", {}).get("timestamp"):
                new["activated"]["datetime"] = slot_status.get("activated", {}).get("timestamp")

            if slot_status.get("activated", {}).get("count"):
                new["activated"]["count"] = slot_status.get("activated", {}).get("count")
            slots.append(new)
    out["slot"] = slots

def add_platform(out):
    platform = {}
    pmap = {
        "NAME": "os-name",
        "VERSION_ID": "os-version",
        "BUILD_ID": "os-release",
        "ARCHITECTURE": "machine"
    }

    os_release = HOST.read("/etc/os-release")
    for line in os_release.splitlines():
        key, value = line.split('=')
        name = pmap.get(key)
        if name:
            platform[name] = value.strip("\"")

    insert(out, "platform", platform)

def add_services(out):
    data = HOST.run_json(["initctl", "-j"], [])
    services = []

    for d in data:
        try:
            services.append({
                "pid": d["pid"],
                "name": d["identity"],
                "status": d["status"],
                "description": d["description"],
                "statistics": {
                    "memory-usage": str(d.get("memory", 0)),
                    "uptime": str(d.get("uptime", 0)),
                    "restart-count": int(d.get("restarts", 0))
                }
            })
        except KeyError:
            continue

    insert(out, "infix-system:services", "service", services)

def add_software(out):
    software = {}
    try:
        data = HOST.run_json(["rauc", "status", "--detailed", "--output-format=json"], {})
        software["compatible"] = data.get("compatible", "")
        software["variant"] = data.get("variant", "")
        software["booted"] = data.get("booted", "")
        boot_order = get_boot_order()
        if not boot_order is None:
            software["boot-order"] = boot_order
        add_software_slots(software, data)
    except subprocess.CalledProcessError:
        pass    # Maybe an upgrade i progress, then rauc does not respond

    installer = {}
    installer_status = HOST.run_json(["rauc-installation-status"], {})
    if installer_status.get("operation", {}):
        installer["operation"] = installer_status["operation"]
    if "progress" in installer_status:
        progress = {}

        if installer_status["progress"].get("percentage"):
            progress["percentage"] = int(installer_status["progress"]["percentage"])
        if installer_status["progress"].get("message"):
            progress["message"] = installer_status["progress"]["message"]
        installer["progress"] = progress
    software["installer"] = installer

    insert(out, "infix-system:software", software)

def add_hostname(out):
    hostname =  HOST.run(tuple(["hostname"]))
    out["hostname"] = hostname.strip()

def add_timezone(out):
    path = HOST.run(tuple("realpath /etc/localtime".split()), "")
    timezone = None
    prefixes = [
        '/usr/share/zoneinfo/posix/',
        '/usr/share/zoneinfo/right/',
        '/usr/share/zoneinfo/'
    ]

    for prefix in prefixes:
        if path is not None and path.startswith(prefix):
            timezone = path[len(prefix):]
            break
    if timezone is not None:
        timezone=timezone.strip()
        pattern = r'Etc/GMT([\+\-]\d{1,2})$'
        match = re.search(pattern, timezone)
        if match:
            offset = -int(match.group(1))
            insert(out, "clock", "timezone-utc-offset", offset)
        else:
            if timezone == "Etc/UTC":
                insert(out, "clock", "timezone-utc-offset", 0)
            else:
                insert(out, "clock", "timezone-name", timezone)


def add_users(out):
    # Map shell paths to YANG identity names
    shell_map = {
        "/bin/bash": "infix-system:bash",
        "/bin/sh": "infix-system:sh",
        "/usr/bin/clish": "infix-system:clish",
        "/bin/false": "infix-system:false",
        "/sbin/nologin": "infix-system:false",
        "/usr/sbin/nologin": "infix-system:false",
    }

    # Get users from /etc/passwd - include users with 1000 <= uid < 10000 (added by confd)
    passwd_output = HOST.run_multiline(["getent", "passwd"], [])
    passwd_users = {}
    for line in passwd_output:
        parts = line.split(':')
        if len(parts) >= 7:
            username = parts[0]
            uid = int(parts[2]) if parts[2].isdigit() else 0
            shell = parts[6].strip()
            if 1000 <= uid < 10000:
                passwd_users[username] = shell_map.get(shell, "infix-system:false")

    # Get password hashes from shadow
    shadow_output = HOST.run_multiline(["getent", "shadow"], [])
    shadow_hashes = {}
    for line in shadow_output:
        parts = line.split(':')
        if len(parts) >= 2:
            username = parts[0]
            password_hash = parts[1]
            # Only include valid password hashes (not locked/disabled)
            if (password_hash and
                not password_hash.startswith('*') and
                not password_hash.startswith('!')):
                shadow_hashes[username] = password_hash

    # Build user list from passwd users (1000 <= uid < 10000)
    users = []
    for username, shell in passwd_users.items():
        user = {"name": username}
        if username in shadow_hashes:
            user["password"] = shadow_hashes[username]
        user["infix-system:shell"] = shell

        # Read SSH authorized keys from /var/run/sshd/${user}.keys
        keys_file = f"/var/run/sshd/{username}.keys"
        keys_content = HOST.read(keys_file)
        if keys_content:
            authorized_keys = []
            for line in keys_content.splitlines():
                line = line.strip()
                if not line or line.startswith('#'):
                    continue
                parts = line.split(None, 2)
                if len(parts) >= 2:
                    algorithm = parts[0]
                    key_data = parts[1]
                    # Use comment as key name, or generate one
                    key_name = parts[2] if len(parts) > 2 else f"{username}-key-{len(authorized_keys)}"
                    authorized_keys.append({
                        "name": key_name,
                        "algorithm": algorithm,
                        "key-data": key_data
                    })
            if authorized_keys:
                user["authorized-key"] = authorized_keys

        users.append(user)

    insert(out, "authentication", "user", users)

def add_clock(out):
    clock = {}
    clock_now=YangDate()

    uptime=HOST.read("/proc/uptime")
    uptime = float(uptime.split()[0])

    clock["boot-datetime"] = str(clock_now.from_seconds(uptime))
    clock["current-datetime"] = str(clock_now)
    insert(out, "clock", clock)

def add_resource_usage(out):
    """Add system resource usage (memory, load average, filesystem) to system-state"""
    resource = {}

    # Memory usage
    try:
        meminfo = HOST.read("/proc/meminfo")
        if not meminfo:
            return
        mem_info = {}
        for line in meminfo.splitlines():
            parts = line.split(":")
            if len(parts) == 2:
                key = parts[0].strip()
                value = parts[1].strip()
                if key in ["MemTotal", "MemFree", "MemAvailable"]:
                    # Store in KiB (as provided by /proc/meminfo, mislabeled as kB)
                    mem_info[key] = int(value.split()[0])

        if mem_info:
            memory = {}
            if "MemTotal" in mem_info:
                memory["total"] = str(mem_info["MemTotal"])
            if "MemFree" in mem_info:
                memory["free"] = str(mem_info["MemFree"])
            if "MemAvailable" in mem_info:
                memory["available"] = str(mem_info["MemAvailable"])
            resource["memory"] = memory
    except (FileNotFoundError, ValueError):
        pass

    # Load average
    try:
        loadavg = HOST.read("/proc/loadavg")
        load_parts = loadavg.strip().split()
        if len(load_parts) >= 3:
            load = {
                "load-1min": load_parts[0],
                "load-5min": load_parts[1],
                "load-15min": load_parts[2]
            }
            resource["load-average"] = load
    except (FileNotFoundError, ValueError):
        pass

    # Filesystem usage
    filesystems = []
    for mount in ["/", "/var", "/cfg"]:
        try:
            result = HOST.run_multiline(["df", "-k", mount], [])
            if len(result) > 1:
                parts = result[1].split()
                if len(parts) >= 4:
                    filesystems.append({
                        "mount-point": mount,
                        "size": str(parts[1]),
                        "used": str(parts[2]),
                        "available": str(parts[3])
                    })
        except (subprocess.CalledProcessError, ValueError, IndexError):
            pass

    if filesystems:
        resource["filesystem"] = filesystems

    if resource:
        insert(out, "infix-system:resource-usage", resource)

def operational():
    out = {
        "ietf-system:system": {
        },
        "ietf-system:system-state": {
        }
    }
    out_state = out["ietf-system:system-state"]
    out_system = out["ietf-system:system"]
    add_hostname(out_system)
    add_users(out_system)
    add_timezone(out_system)
    add_software(out_state)
    add_ntp(out_state)
    add_dns(out_state)
    add_clock(out_state)
    add_platform(out_state)
    add_services(out_state)
    add_resource_usage(out_state)

    return out
