#!/usr/bin/python

import asyncio
import re
import sys
try:
    import confluent.asynclient as asynclient
except ImportError:
    sys.path.append('/opt/confluent/lib/python')
    import confluent.asynclient as asynclient

_whitelistnames = (
    # 3com
    re.compile(r'^RMON Port (\d+) on unit \d+'),
    # Dell
    re.compile(r'^Unit \d+ Port (\d+)\Z'),
)

_blacklistnames = (
    re.compile(r'vl'),
    re.compile(r'Nu'),
    re.compile(r'RMON'),
    re.compile(r'onsole'),
    re.compile(r'Stack'),
    re.compile(r'Trunk'),
    re.compile(r'po\d'),
    re.compile(r'XGE'),
    re.compile(r'LAG'),
    re.compile(r'CPU'),
    re.compile(r'Management'),
)

def mac2lla(mac):
    """Convert MAC address to IPv6 link-local address."""
    # Remove colons from MAC address
    mac_clean = mac.replace(':', '')
    # Insert fe80:: prefix and convert to lowercase
    # Split MAC into first 3 octets and last 3 octets
    first_half = mac_clean[:6]
    second_half = mac_clean[6:]
    # Flip the universal/local bit in the first octet
    first_octet = int(first_half[:2], 16)
    first_octet ^= 0x02
    first_half = f'{first_octet:02x}' + first_half[2:]
    # Format as IPv6 link-local address
    lla = f'fe80::{first_half[0:4]}:{first_half[4:]}ff:fe{second_half[0:2]}:{second_half[2:6]}'
    return lla

async def is_reachable(address):
    try:
        await asyncio.wait_for(asyncio.open_connection(address, 22), timeout=0.5)
        return True
    except (asyncio.TimeoutError, OSError):
        return False

async def get_nics():
    proc = await asyncio.create_subprocess_exec(
        'ip', '-json', 'link',
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE
    )
    stdout, _ = await proc.communicate()
    import json
    links = json.loads(stdout)
    for link in links:
        if 'LOOPBACK' in link.get('flags', []):
            continue
        if 'LOWER_UP' not in link.get('flags', []):
            continue
        yield link['ifname']

async def add_zone(lla):
    if '%' in lla:
        return lla
    tocheck = []
    async for link in get_nics():
        tocheck.append(link)
    tasks = []
    for link in tocheck:
        zone_addr = lla + '%' + link
        tasks.append(is_reachable(zone_addr))
    
    results = await asyncio.gather(*tasks)
    for link, reachable in zip(tocheck, results):
        if reachable:
            return lla + '%' + link
    
    return lla

def _namesmatch(switchdesc, userdesc):
    if switchdesc is None:
        return False
    if switchdesc == userdesc:
        return True
    try:
        portnum = int(userdesc)
    except ValueError:
        portnum = None
    if portnum is not None:
        for exp in _whitelistnames:
            match = exp.match(switchdesc)
            if match:
                snum = int(match.groups()[0])
                if snum == portnum:
                    return True
    anymatch = re.search(r'[^0123456789]' + userdesc + r'(\.0)?\Z', switchdesc)
    if anymatch:
        for blexp in _blacklistnames:
            if blexp.match(switchdesc):
                return False
        return True
    return False

async def ping_everywhere():
    tasks = []
    async for link in get_nics():
        tasks.append(asyncio.create_subprocess_exec(
            'ping', '-I', link, 'ff02::1', '-c', '2',
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        ))
    
    for task in tasks:
        proc = await task
        await proc.communicate()
        
async def main(switch, port):
    portname = None
    client = asynclient.Command()
    async for rsp in client.read(f'/networking/neighbors/by-switch/{switch}/by-port/'):
        portcandidate = rsp.get('item', {}).get('href')[:-1]
        if _namesmatch(portcandidate, port):
            if portname:
                sys.stderr.write(f"Multiple matches found for port {port} on switch {switch}\n")
                portname = None
            else:
                portname = portcandidate
    peerid = None
    if portname:
        async for rsp in client.read(f'/networking/neighbors/by-switch/{switch}/by-port/{portname}/by-peerid/'):
            peerid = rsp.get('item', {}).get('href')
    fe80found = False
    if peerid:
        maybemac = None
        async for rsp in client.read(f'/networking/neighbors/by-switch/{switch}/by-port/{portname}/by-peerid/{peerid}'):
            if 'peeraddresses' in rsp:
                for addr in rsp['peeraddresses']:
                    if addr.startswith('fe80::'):
                        fe80found = True
                        addr = await add_zone(addr)
                    print(addr)
            if 'peerchassisid' in rsp:
                maybemac = rsp['peerchassisid']
        if not fe80found and maybemac:
            try:
                maybella = mac2lla(maybemac)
                maybella = await add_zone(maybella)
                fe80found = True
                print(maybella)
            except Exception as e:
                pass
    if not fe80found:
        if not portname:
            async for rsp in client.read(f'/networking/macs/by-switch/{switch}/by-port/'):
                portcandidate = rsp.get('item', {}).get('href')[:-1]
                if _namesmatch(portcandidate, port):
                    if portname:
                        sys.stderr.write(f"Multiple matches found for port {port} on switch {switch}\n")
                        portname = None
                    else:
                        portname = portcandidate
        if not portname:
            await ping_everywhere()
            async for rsp in client.update(f'/networking/macs/rescan', {'rescan': 'start'}):
                pass
            scanning = True
            while scanning:
                async for rsp in client.read(f'/networking/macs/rescan'):
                    if 'scanning' in rsp:
                        scanning = rsp['scanning']
                if scanning:
                    await asyncio.sleep(1)
        async for rsp in client.read(f'/networking/macs/by-switch/{switch}/by-port/{portname}/by-mac/'):
            mac = rsp.get('item', {}).get('href').replace('-', ':')
            if mac:
                try:
                    maybella = mac2lla(mac)
                    maybella = await add_zone(maybella)
                    fe80found = True
                    print(maybella)
                except Exception as e:
                    pass

            
    


if __name__ == '__main__':
    if len(sys.argv) < 3:
        sys.stderr.write(f"Usage: {sys.argv[0]} <switch> <port>\n")
        sys.exit(1)
    switch = sys.argv[1]
    port = sys.argv[2]
    asyncio.run(main(switch, port))

