#!/usr/bin/python3
"""Persist a per-device OneCloud MAC in netifd configuration before DHCP."""
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
import sys
import tempfile

PROFILES = {
    'onecloud': ('OCP', '/sys/bus/nvmem/devices/meson8b-efuse0/nvmem'),
    'onecloud-pro': ('ODC', '/sys/devices/platform/efuse/efuse0/nvmem'),
}
MAC_FILE = Path('/etc/one-kvm-image/eth0.mac')
LEGACY_FILE = Path('/etc/systemd/network/99-eth0.network')


def valid_mac(value):
    return (bool(re.fullmatch(r'(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}', value))
            and not int(value[:2], 16) & 1
            and value.lower() != '00:00:00:00:00:00')


def derive_mac(data, prefix):
    offset = data.find(prefix.encode('ascii'))
    if offset < 0:
        return None
    serial = data[offset:offset + 13]
    if len(serial) != 13 or any(byte < 33 or byte > 126 for byte in serial):
        return None
    tail = hashlib.sha256(serial).digest()[-5:]
    return '02:' + ':'.join(f'{byte:02x}' for byte in tail)


def generate_mac(board):
    prefix, preferred = PROFILES[board]
    candidates = [Path(preferred), *sorted(Path('/sys/bus/nvmem/devices').glob('*efuse*/nvmem'))]
    for path in dict.fromkeys(candidates):
        try:
            mac = derive_mac(path.read_bytes(), prefix)
        except OSError:
            continue
        if mac:
            return mac
    return '02:' + ':'.join(f'{byte:02x}' for byte in os.urandom(5))


def parse_network(text):
    sections = {}
    for line in text.splitlines():
        key, separator, value = line.partition('=')
        parts = key.split('.')
        if not separator or len(parts) not in (2, 3) or parts[0] != 'network':
            continue
        if not re.fullmatch(r'[A-Za-z0-9_]+', parts[1]):
            continue
        if value.startswith("'") and value.endswith("'"):
            value = value[1:-1]
        section = sections.setdefault(parts[1], {})
        section[parts[2] if len(parts) == 3 else '.type'] = value
    return sections


def select_mac(board, sections, mac_file=MAC_FILE, legacy_file=LEGACY_FILE):
    if mac_file.exists():
        mac = mac_file.read_text().strip()
        if not valid_mac(mac):
            raise ValueError(f'Invalid persisted MAC in {mac_file}')
        return mac.lower()
    # Preserve an explicit OpenWrt address when installing on an existing system.
    existing = [section.get('macaddr', '') for section in sections.values()
                if section.get('.type') == 'device' and section.get('name') == 'eth0']
    existing.append(sections.get('lan', {}).get('macaddr', ''))
    if legacy_file.is_file():
        existing.extend(line.split('=', 1)[1].strip() for line in legacy_file.read_text().splitlines()
                        if line.startswith('MACAddress='))
    for value in existing:
        if valid_mac(value):
            return value.lower()
    return generate_mac(board)


def persist_mac(path, mac):
    if path.is_file() and path.read_text().strip() == mac:
        return
    path.parent.mkdir(parents=True, exist_ok=True)
    temporary = None
    try:
        with tempfile.NamedTemporaryFile(mode='w', dir=path.parent, prefix=path.name + '.',
                                         delete=False) as stream:
            temporary = Path(stream.name)
            stream.write(mac + '\n')
            stream.flush()
            os.fchmod(stream.fileno(), 0o644)
            os.fsync(stream.fileno())
        os.replace(temporary, path)
        temporary = None
        directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
        try:
            os.fsync(directory)
        finally:
            os.close(directory)
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)


def configure(board, mac_file=MAC_FILE, legacy_file=LEGACY_FILE):
    if board not in PROFILES:
        return None
    result = subprocess.run(['uci', '-q', '-X', 'show', 'network'], check=True,
                            capture_output=True, text=True)
    sections = parse_network(result.stdout)
    devices = [name for name, section in sections.items()
               if section.get('.type') == 'device' and section.get('name') == 'eth0']
    if len(devices) > 1:
        raise ValueError('Multiple eth0 device sections; resolve duplicate network configuration')
    section = devices[0] if devices else 'one_kvm_eth0'
    if not devices and section in sections:
        raise ValueError('network.one_kvm_eth0 already belongs to another device')
    mac = select_mac(board, sections, mac_file, legacy_file)
    # Keep the identity even if UCI commit fails and first-boot setup retries.
    persist_mac(mac_file, mac)
    if not devices:
        subprocess.run(['uci', 'set', f'network.{section}=device'], check=True)
        subprocess.run(['uci', 'set', f'network.{section}.name=eth0'], check=True)
    if sections.get(section, {}).get('macaddr', '').lower() != mac:
        subprocess.run(['uci', 'set', f'network.{section}.macaddr={mac}'], check=True)
        subprocess.run(['uci', 'commit', 'network'], check=True)
    print(f'Persistent eth0 MAC: {mac}', flush=True)
    return mac


if __name__ == '__main__':
    try:
        board = json.loads(Path('/etc/one-kvm-os-release').read_text())['board']
        configure(board)
    except (OSError, ValueError, KeyError, subprocess.CalledProcessError) as error:
        print(f'one-kvm-stable-mac: {error}', file=sys.stderr)
        sys.exit(1)
