#!/usr/bin/env python3

import requests
import json
import argparse
import sys
from urllib.parse import urljoin
from mikrotik_connect import MikrotikAPI


class NetBoxSync:
    def __init__(self, netbox_url, netbox_token):
        self.base_url = netbox_url.rstrip('/')
        self.api_url = urljoin(self.base_url + '/', 'api/')
        self.headers = {
            'Authorization': f'Token {netbox_token}',
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        }
        self.session = requests.Session()
        self.session.headers.update(self.headers)
    
    def post(self, endpoint, data):
        """Make POST request to NetBox API"""
        url = urljoin(self.api_url, endpoint.lstrip('/'))
        response = self.session.post(url, json=data)
        if response.status_code not in [200, 201]:
            print(f"Error creating {endpoint}: {response.status_code}")
            print(f"Response: {response.text}")
        response.raise_for_status()
        return response.json()
    
    def patch(self, endpoint, data):
        """Make PATCH request to NetBox API"""
        url = urljoin(self.api_url, endpoint.lstrip('/'))
        response = self.session.patch(url, json=data)
        if response.status_code not in [200, 201]:
            print(f"Error updating {endpoint}: {response.status_code}")
            print(f"Response: {response.text}")
        response.raise_for_status()
        return response.json()
    
    def get(self, endpoint, params=None):
        """Make GET request to NetBox API"""
        url = urljoin(self.api_url, endpoint.lstrip('/'))
        response = self.session.get(url, params=params)
        response.raise_for_status()
        return response.json()
    
    def get_or_create_prefix_role(self, role_name):
        """Get or create a prefix role"""
        # Check if role exists
        role_response = self.get('ipam/roles/', params={'name': role_name})
        if role_response['count'] > 0:
            return role_response['results'][0]['id']
        
        # Create role
        role_data = {
            'name': role_name,
            'slug': role_name.lower().replace(' ', '-')
        }
        created_role = self.post('ipam/roles/', role_data)
        return created_role['id']
    
    def determine_prefix_role(self, interface_name, ip_address):
        """Determine the role of a prefix based on interface and IP"""
        interface_lower = interface_name.lower()
        
        if 'loopback' in interface_lower:
            return 'Loopback'
        elif any(mgmt in interface_lower for mgmt in ['mgmt', 'management', 'ether1']):
            return 'Management'
        elif any(infra in interface_lower for infra in ['tower', 'backhaul', 'climax', 'switch']):
            return 'Infrastructure'
        elif ip_address.startswith('100.64.'):
            return 'CGNAT'
        elif ip_address.startswith('10.10.'):
            return 'CPE-Management'
        elif any(cust in interface_lower for cust in ['customer', 'cpe', 'vlan']):
            return 'Customer'
        else:
            return 'Unknown'
    
    def determine_interface_type(self, interface_name):
        """Determine the NetBox interface type based on MikroTik interface name"""
        interface_lower = interface_name.lower()
        
        # Check for specific interface types first (most specific to least specific)
        if 'loopback' in interface_lower or interface_lower == 'lo':
            return 'virtual'
        elif 'vlan' in interface_lower:
            return 'virtual'
        elif 'bond' in interface_lower or 'bonding' in interface_lower:
            return 'lag'
        elif 'pppoe' in interface_lower:
            return 'virtual'
        elif 'sfp-sfpplus' in interface_lower or 'sfpplus' in interface_lower:
            return '10gbase-x-sfpp'
        elif 'sfp' in interface_lower:
            return '1000base-x-sfp'
        elif 'ether' in interface_lower:
            return '1000base-t'
        elif 'wlan' in interface_lower or 'wireless' in interface_lower:
            return 'ieee802.11n'
        elif 'bridge' in interface_lower:
            return 'bridge'
        # Only check for exact matches for bridge names
        elif interface_lower in ['verona', 'lowry', 'culleoka', 'climax', 'yorkshire', 'new-hope']:
            return 'bridge'
        else:
            return 'other'
    
    def sync_router_to_netbox(self, router_ip, router_user, router_pass, site_name, device_name=None):
        """Sync router configuration to NetBox"""
        
        # Connect to router and get data
        print(f"Connecting to router {router_ip}...")
        api = MikrotikAPI(router_ip, router_user, router_pass)
        
        try:
            if not api.connect():
                print("Failed to connect to router")
                return False
            
            if not api.login():
                print("Failed to login to router")
                return False
            
            # Get all IP addresses from router
            addresses = api.command("/ip/address/print")
            print(f"Retrieved {len(addresses)} IP addresses from router")
            
        finally:
            api.disconnect()
        
        # Get site from NetBox
        site_response = self.get('dcim/sites/', params={'name': site_name})
        if site_response['count'] == 0:
            print(f"Error: Site '{site_name}' not found in NetBox")
            return False
        
        site_id = site_response['results'][0]['id']
        print(f"Found site '{site_name}' with ID: {site_id}")
        
        # Get device if specified
        device_id = None
        if device_name:
            device_response = self.get('dcim/devices/', params={'name': device_name})
            if device_response['count'] > 0:
                device_id = device_response['results'][0]['id']
                print(f"Found device '{device_name}' with ID: {device_id}")
        else:
            # Try to find a device at this site
            device_response = self.get('dcim/devices/', params={'site_id': site_id})
            if device_response['count'] > 0:
                device_id = device_response['results'][0]['id']
                device_name = device_response['results'][0]['name']
                print(f"Found device '{device_name}' at site")
        
        # Process addresses (excluding dynamic PPPoE)
        prefixes_to_create = {}
        ips_to_update = []
        
        for addr in addresses:
            # Skip disabled and dynamic addresses
            if addr.get('disabled', 'false') == 'true':
                continue
            if addr.get('dynamic', 'false') == 'true' and 'pppoe' in addr.get('interface', '').lower():
                continue
            
            interface = addr.get('interface', 'unknown')
            address = addr.get('address', '')
            network = addr.get('network', '')
            
            if not address or not network:
                continue
            
            # Calculate prefix
            prefix_bits = address.split('/')[-1]
            prefix = f"{network}/{prefix_bits}"
            
            # Store unique prefixes
            if prefix not in prefixes_to_create and prefix != f"{network}/32":
                prefixes_to_create[prefix] = {
                    'interface': interface,
                    'role': self.determine_prefix_role(interface, address)
                }
            
            # Store IPs to update
            ips_to_update.append({
                'address': address,
                'interface': interface,
                'prefix': prefix
            })
        
        # Create/Update prefixes
        print(f"\n=== Processing {len(prefixes_to_create)} Prefixes ===")
        prefix_stats = {'created': 0, 'updated': 0, 'failed': 0}
        
        for prefix, info in prefixes_to_create.items():
            # Check if prefix exists
            prefix_response = self.get('ipam/prefixes/', params={'prefix': prefix})
            
            prefix_data = {
                'prefix': prefix,
                'site': site_id,
                'status': 'active',
                'description': f"{site_name} - {info['interface']}",
                'is_pool': False
            }
            
            # Add role
            try:
                role_id = self.get_or_create_prefix_role(info['role'])
                prefix_data['role'] = role_id
            except:
                pass
            
            try:
                if prefix_response['count'] == 0:
                    # Create new prefix
                    self.post('ipam/prefixes/', prefix_data)
                    print(f"Created prefix: {prefix} ({info['role']})")
                    prefix_stats['created'] += 1
                else:
                    # Update existing prefix
                    existing_id = prefix_response['results'][0]['id']
                    self.patch(f'ipam/prefixes/{existing_id}/', prefix_data)
                    print(f"Updated prefix: {prefix} ({info['role']})")
                    prefix_stats['updated'] += 1
            except Exception as e:
                print(f"Failed to process prefix {prefix}: {e}")
                prefix_stats['failed'] += 1
        
        print(f"\nPrefix Summary: {prefix_stats['created']} created, {prefix_stats['updated']} updated, {prefix_stats['failed']} failed")
        
        # Update IP addresses
        print(f"\n=== Processing {len(ips_to_update)} IP Addresses ===")
        ip_stats = {'created': 0, 'updated': 0, 'failed': 0}
        
        for ip_info in ips_to_update:
            # Check if IP exists
            ip_response = self.get('ipam/ip-addresses/', params={'address': ip_info['address']})
            
            ip_data = {
                'address': ip_info['address'],
                'status': 'active',
                'description': f"{ip_info['interface']}",
                'tenant': None
            }
            
            try:
                if ip_response['count'] == 0:
                    # Create new IP
                    ip_data['site'] = site_id
                    self.post('ipam/ip-addresses/', ip_data)
                    print(f"Created IP: {ip_info['address']} ({ip_info['interface']})")
                    ip_stats['created'] += 1
                else:
                    # Update existing IP
                    existing_ip = ip_response['results'][0]
                    existing_id = existing_ip['id']
                    
                    # Only update if needed
                    if existing_ip.get('site', {}).get('id') != site_id or existing_ip.get('description') != ip_data['description']:
                        ip_data['site'] = site_id
                        self.patch(f'ipam/ip-addresses/{existing_id}/', ip_data)
                        print(f"Updated IP: {ip_info['address']} ({ip_info['interface']})")
                        ip_stats['updated'] += 1
                
                # Create interface if device exists
                if device_id:
                    interface_name = ip_info['interface']
                    
                    # Check if interface exists
                    interface_response = self.get('dcim/interfaces/', params={
                        'device_id': device_id,
                        'name': interface_name
                    })
                    
                    if interface_response['count'] == 0:
                        # Create interface
                        interface_data = {
                            'device': device_id,
                            'name': interface_name,
                            'type': self.determine_interface_type(interface_name),
                            'enabled': True
                        }
                        try:
                            created_interface = self.post('dcim/interfaces/', interface_data)
                            interface_id = created_interface['id']
                            
                            # Assign IP to interface
                            ip_id = self.get('ipam/ip-addresses/', params={'address': ip_info['address']})['results'][0]['id']
                            self.patch(f'ipam/ip-addresses/{ip_id}/', {
                                'assigned_object_type': 'dcim.interface',
                                'assigned_object_id': interface_id
                            })
                            print(f"  - Created interface: {interface_name} (Type: {interface_data['type']})")
                        except Exception as e:
                            print(f"  - Failed to create interface {interface_name}: {e}")
                
            except Exception as e:
                print(f"Failed to process IP {ip_info['address']}: {e}")
                ip_stats['failed'] += 1
        
        print(f"\nIP Summary: {ip_stats['created']} created, {ip_stats['updated']} updated, {ip_stats['failed']} failed")
        
        print("\n=== Sync Complete ===")
        return True


def main():
    parser = argparse.ArgumentParser(description='Sync MikroTik router configuration to NetBox')
    parser.add_argument('router_ip', help='Router IP address')
    parser.add_argument('site_name', help='NetBox site name')
    parser.add_argument('--router-user', default='grahamro', help='Router username (default: grahamro)')
    parser.add_argument('--router-pass', default='cFKhz8q5gPLoucMbcT1Iy58r3IXgc3', help='Router password')
    parser.add_argument('--device-name', help='NetBox device name (optional)')
    parser.add_argument('--netbox-url', default='https://netbox.vntx.net/', help='NetBox URL')
    parser.add_argument('--netbox-token', default='e50298f7fd20f7fd6f1931f635511b34f6e8cfde', help='NetBox API token')
    
    args = parser.parse_args()
    
    # Create sync instance
    sync = NetBoxSync(args.netbox_url, args.netbox_token)
    
    # Run sync
    success = sync.sync_router_to_netbox(
        args.router_ip,
        args.router_user,
        args.router_pass,
        args.site_name,
        args.device_name
    )
    
    return 0 if success else 1


if __name__ == "__main__":
    sys.exit(main())