import os
import json
import traceback
import sys
import ipaddress
import csv
import shutil
from datetime import datetime
from graphql_client.api.call_api import ApiClient, CallApi
from graphql_client.api_client import ApiException
from ..customLib import writeDataToFile, makeCall, getAccountID, clean_csv_file
from ....Utils.cliutils import load_cli_settings

def generate_template(args):
    """
    Generate template files from embedded templates directory
    """
    try:
        # Get the directory of this script to locate templates
        script_dir = os.path.dirname(os.path.abspath(__file__))
        
        # Try multiple possible template directory locations
        # 1. Development mode: go up 4 directories
        # 2. Installed package: templates should be in the package root
        possible_template_dirs = [
            os.path.join(script_dir, '..', '..', '..', '..', 'templates'),  # Development
            os.path.join(os.path.dirname(script_dir.split('catocli')[0]), 'catocli', 'templates'),  # Installed package
        ]
        
        templates_dir = None
        for potential_dir in possible_template_dirs:
            normalized_dir = os.path.normpath(potential_dir)
            if os.path.exists(normalized_dir):
                templates_dir = normalized_dir
                break
        
        if templates_dir is None:
            # Fallback: look for templates directory in catocli package
            import catocli
            catocli_root = os.path.dirname(os.path.abspath(catocli.__file__))
            fallback_templates_dir = os.path.join(catocli_root, 'templates')
            if os.path.exists(fallback_templates_dir):
                templates_dir = fallback_templates_dir
            else:
                raise Exception(f"Templates directory not found. Searched locations: {possible_template_dirs} and {fallback_templates_dir}")
        
        # Get the template format from export_format argument (defaults to json)
        template_format = getattr(args, 'export_format', 'json')
        
        # Determine output directory
        output_dir = getattr(args, 'output_directory', None)
        if output_dir is None:
            output_dir = os.getcwd()
        elif not os.path.isabs(output_dir):
            output_dir = os.path.join(os.getcwd(), output_dir)
        
        os.makedirs(output_dir, exist_ok=True)
        
        copied_files = []
        current_dir = os.getcwd()
        
        if template_format == 'csv':
            # Copy main socket_sites.csv template
            main_template = 'socket_sites.csv'
            src_path = os.path.join(templates_dir, main_template)
            dst_path = os.path.join(output_dir, main_template)
            
            if os.path.exists(src_path):
                shutil.copy2(src_path, dst_path)
                copied_files.append(dst_path)
                if hasattr(args, 'verbose') and args.verbose:
                    print(f"Copied template: {src_path} -> {dst_path}")
            else:
                print(f"Warning: Template file not found: {src_path}")
            
            # Copy network_ranges template files from sites_config subdirectory
            sites_config_dir = os.path.join(templates_dir, 'sites_config')
            if os.path.exists(sites_config_dir) and os.path.isdir(sites_config_dir):
                # Create sites_config subdirectory in output
                output_sites_config = os.path.join(output_dir, 'sites_config')
                os.makedirs(output_sites_config, exist_ok=True)
                
                # Copy all network_ranges CSV files
                for filename in os.listdir(sites_config_dir):
                    if filename.endswith('_network_ranges.csv'):
                        src_file = os.path.join(sites_config_dir, filename)
                        dst_file = os.path.join(output_sites_config, filename)
                        shutil.copy2(src_file, dst_file)
                        copied_files.append(dst_file)
                        if hasattr(args, 'verbose') and args.verbose:
                            print(f"Copied template: {src_file} -> {dst_file}")
            else:
                print(f"Warning: sites_config directory not found: {sites_config_dir}")
        
        elif template_format == 'json':
            # Copy JSON template file
            template_file = 'socket_sites.json'
            src_path = os.path.join(templates_dir, template_file)
            dst_path = os.path.join(output_dir, template_file)
            
            if os.path.exists(src_path):
                shutil.copy2(src_path, dst_path)
                copied_files.append(dst_path)
                if hasattr(args, 'verbose') and args.verbose:
                    print(f"Copied template: {src_path} -> {dst_path}")
            else:
                print(f"Warning: Template file not found: {src_path}")
        
        if copied_files:
            print(f"Successfully generated {template_format.upper()} template files:")
            # Display relative paths if files are in current directory, otherwise absolute
            for file_path in copied_files:
                try:
                    rel_path = os.path.relpath(file_path, current_dir)
                    # If relative path doesn't go up directories, use it; otherwise use absolute
                    if not rel_path.startswith('..'):
                        print(f"  {rel_path}")
                    else:
                        print(f"  {file_path}")
                except ValueError:
                    # On Windows, relpath can fail if paths are on different drives
                    print(f"  {file_path}")
            return [{"success": True, "template_format": template_format, "copied_files": copied_files}]
        else:
            return [{"success": False, "error": f"No template files found for format: {template_format}"}]
    
    except Exception as e:
        print(f"Error generating template: {str(e)}")
        return [{"success": False, "error": str(e)}]

def export_socket_sites_dispatcher(args, configuration):
    """
    Dispatcher function that routes to JSON or CSV export based on format argument
    """
    # Check if template generation is requested
    if hasattr(args, 'generate_template') and args.generate_template:
        return generate_template(args)
    
    export_format = getattr(args, 'export_format', 'json')
    
    if export_format == 'csv':
        return export_socket_site_to_csv(args, configuration)
    else:
        return export_socket_site_to_json(args, configuration)


def export_socket_site_to_json(args, configuration):
    """
    Export consolidated site and socket data to JSON format
    """
    processed_data = {'sites':[]}
    warning_stats = {
        'missing_sites': 0,
        'missing_interfaces': 0,
        'missing_data': 0,
        'missing_interface_details': []
    }

    try:
        # Load CLI settings using the robust function
        settings = load_cli_settings()
        # Note: load_cli_settings() now returns embedded defaults if file cannot be loaded
        
        account_id = getAccountID(args, configuration)

        # Get siteIDs from args if provided
        site_ids = []
        if hasattr(args, 'siteIDs') and args.siteIDs:
            site_ids = [site_id.strip() for site_id in args.siteIDs.split(',') if site_id.strip()]
        
        if hasattr(args, 'verbose') and args.verbose:
            if site_ids:
                print(f"Found {len(site_ids)} site(s) matching the provided site IDs")
            else:
                print(f"Found {len(site_ids)} site(s) in account")

        if not site_ids:
            site_ids = None

        ###############################################################
        ## Call APIs to retrieve sites, interface and network ranges ##
        ###############################################################
        entity_sites = getEntityLookup(args, configuration, account_id, "site", site_ids, None)
        if not entity_sites or len(entity_sites) == 0:
            if site_ids:
                # User provided specific site IDs but none were found
                print(f"No sites found matching the provided site IDs: {', '.join(site_ids)}")
                print("Please verify the site IDs are correct and that they exist in this account.")
                return [{"success": False, "message": f"No sites found for the specified site IDs: {', '.join(site_ids)}", "sites_requested": site_ids}]
            else:
                # No site filter was provided but no sites exist at all
                print("No sites found in this account.")
                return [{"success": False, "message": "No sites found in account", "account_id": account_id}]
        
        total_sites = len(entity_sites)
        print(f"\nExporting Cato physical socket sites:\n")
        print(f"{total_sites} site(s) found...\n")
        
        for idx, entity_site in enumerate(entity_sites, 1):
            site_id = entity_site["entity"]["id"]
            entity_input = {"id":site_id,"type":"site"}
            snapshot_response = getAccountSnapshot(args, configuration, account_id, [site_id])
            snapshot_sites = snapshot_response['data']['accountSnapshot']['sites']
            snapshot_site = snapshot_sites[0] if snapshot_sites else {}
            entity_network_interfaces = getEntityLookup(args, configuration, account_id, "networkInterface",None, entity_input)
            entity_network_ranges = getEntityLookup(args, configuration, account_id, "siteRange",None, entity_input)

            connectionType = snapshot_site.get('infoSiteSnapshot', {}).get('connType', "")
            site_name = snapshot_site.get('infoSiteSnapshot', {}).get('name', 'Unknown')

            cur_site = {
                'wan_interfaces': [],
                'lan_interfaces': [],
                'native_range': {}
            }

            if connectionType not in settings["export_by_socket_type"]:
                print(f"[{idx}/{total_sites}] Ignoring cloud site: {site_name} (ID: {site_id})")
            else:
                print(f"[{idx}/{total_sites}] Processing physical socket site: {site_name} (ID: {site_id})")
                cur_site['id'] = site_id
                cur_site['name'] = snapshot_site.get('infoSiteSnapshot', {}).get('name')
                cur_site['description'] = snapshot_site.get('infoSiteSnapshot', {}).get('description')
                cur_site['connection_type'] = connectionType
                cur_site['type'] = snapshot_site.get('infoSiteSnapshot', {}).get('type')
                cur_site['license_id'] = None
                cur_site['license_bw'] = None
                cur_site = populateSiteLocationData(args, snapshot_site, cur_site)
                
                # Create a map of interfaces from account snapshot for native range lookup
                site_interfaces = snapshot_site.get('infoSiteSnapshot', {}).get('interfaces', [])
                interface_lookup = {}  # Map interface ID to interface data

                # Build precedence map from devices.interfaces.naturalOrder
                # naturalOrder: 1 = ACTIVE, 2 = PASSIVE, 3 = LAST_RESORT
                precedence_map = {}  # Map interface ID to precedence string
                site_devices = snapshot_site.get('devices') or []
                for device in site_devices:
                    device_interfaces = device.get('interfaces', [])
                    for device_iface in device_interfaces:
                        iface_id = device_iface.get('id')
                        natural_order = device_iface.get('naturalOrder')
                        if iface_id and natural_order:
                            if natural_order == 1:
                                precedence_map[iface_id] = "ACTIVE"
                            elif natural_order == 2:
                                precedence_map[iface_id] = "PASSIVE"
                            elif natural_order == 3:
                                precedence_map[iface_id] = "LAST_RESORT"

                if hasattr(args, 'verbose') and args.verbose:
                    print(f"DEBUG: Processing site {site_id} ({cur_site['name']}) with {len(site_interfaces)} interfaces")
                    print(f"DEBUG: Built precedence map with {len(precedence_map)} entries: {precedence_map}")

                for interface in site_interfaces:
                    role = interface.get('wanRoleInterfaceInfo', "")
                    dest_type = interface.get('destType', "")
                    interfaceName = interface.get('id', "")
                    
                    # Store interface data for lookup
                    interface_lookup[interfaceName] = {
                        'dest_type': dest_type,
                        'name': interface.get('name', ""),
                        'role': role
                    }
                    
                    # Process WAN interfaces
                    if role is not None and role[0:3] == "wan":
                        cur_wan_interface = {}
                        if interfaceName[0:3] in ("WAN", "USB", "LTE"):
                            cur_wan_interface['id'] = site_id+":"+ interface.get('id', "")
                        else:
                            cur_wan_interface['id'] = site_id+":INT_"+ interface.get('id', "")
                        # Format WAN interface index: INT_X for numeric values, keep as-is for non-numeric
                        wan_interface_id = interface.get('id', "")
                        if isinstance(wan_interface_id, (int, str)) and str(wan_interface_id).isdigit():
                            cur_wan_interface['index'] = f"INT_{wan_interface_id}"
                        else:
                            cur_wan_interface['index'] = wan_interface_id
                        cur_wan_interface['name'] = interface.get('name', "")
                        cur_wan_interface['upstream_bandwidth'] = interface.get('upstreamBandwidth', 0)
                        cur_wan_interface['downstream_bandwidth'] = interface.get('downstreamBandwidth', 0)
                        cur_wan_interface['dest_type'] = dest_type
                        cur_wan_interface['role'] = role
                        # Get precedence from naturalOrder mapping, default to ACTIVE if not found
                        cur_wan_interface['precedence'] = precedence_map.get(wan_interface_id, "ACTIVE")
                        cur_site['wan_interfaces'].append(cur_wan_interface)
                    
                    # Process LAN_LAG_MEMBER interfaces
                    elif dest_type == "LAN_LAG_MEMBER":
                        if hasattr(args, 'verbose') and args.verbose:            
                            print(f"DEBUG: Processing LAN_LAG_MEMBER interface for site {site_id}: {interface.get('name', '')} (id: {interface.get('id', '')})")
                        cur_lan_interface = {
                            'network_ranges': []
                        }
                        # LAN_LAG_MEMBER interfaces don't have a numeric ID, only index and name
                        cur_lan_interface['id'] = ''  # No ID for LAN_LAG_MEMBER
                        cur_lan_interface['name'] = interface.get('name', "")
                        # Format interface index: INT_X for numeric values, keep as-is for non-numeric
                        interface_id = interface.get('id', "")
                        if isinstance(interface_id, (int, str)) and str(interface_id).isdigit():
                            cur_lan_interface['index'] = f"INT_{interface_id}"
                        else:
                            cur_lan_interface['index'] = interface_id
                        cur_lan_interface['dest_type'] = dest_type
                        cur_site['lan_interfaces'].append(cur_lan_interface)
                        if hasattr(args, 'verbose') and args.verbose:
                            print(f"DEBUG: Added LAN_LAG_MEMBER interface: {cur_lan_interface}")
                
                # Store the interface lookup for later use
                cur_site['_interface_lookup'] = interface_lookup

                if site_id:
                    processed_data['sites'].append(cur_site)
                    if hasattr(args, 'verbose') and args.verbose:        
                        print(f"DEBUG: Added site {site_id} ({cur_site['name']}) with {len(cur_site['lan_interfaces'])} LAN interfaces")

            ##################################################################################
            ## Process entity lookup LAN network interfaces adding to site object by site_id##
            ##################################################################################
            for lan_ni in entity_network_interfaces:
                # Only add interface if the site exists in processed_data
                lan_ni_helper_fields = lan_ni.get("helperFields", {})
                lan_ni_entity_data = lan_ni.get('entity', {})
                lan_ni_site_id = str(lan_ni_helper_fields.get('siteId', ""))
                cur_site_entry = next((site for site in processed_data['sites'] if site['id'] == lan_ni_site_id), None)
                if cur_site_entry:
                    cur_lan_interface = {'network_ranges': []}
                    ni_interface_id = lan_ni_entity_data.get('id', "")
                    ni_interface_name = lan_ni_helper_fields.get('interfaceName', "")
                    lan_ni_subnet = str(lan_ni_helper_fields.get('subnet', ""))
                    ni_index = lan_ni_helper_fields.get('interfaceId', "")
                    ni_index = f"INT_{ni_index}" if isinstance(ni_index, (int, str)) and str(ni_index).isdigit() else ni_index
                    # Use isDefault flag from API instead of hardcoded map
                    is_default_interface = lan_ni_helper_fields.get('isDefault', False)

                    if is_default_interface:
                        cur_site_entry["native_range"]["interface_id"] = ni_interface_id
                        cur_site_entry["native_range"]["interface_name"] = ni_interface_name
                        cur_site_entry["native_range"]["subnet"] = lan_ni_subnet
                        cur_site_entry["native_range"]["index"] = ni_index
                        
                        # Get interface details from the stored lookup data
                        interface_lookup = cur_site_entry.get('_interface_lookup', {})
                        interface_details = interface_lookup.get(str(lan_ni_helper_fields.get('interfaceId', '')), {})
                        native_range_dest_type = interface_details.get('dest_type', '')
                        cur_site_entry["native_range"]["dest_type"] = native_range_dest_type
                        
                        # Set lag_min_links to 1 only for LAN_LAG_MASTER interfaces
                        lag_min_links = None
                        if 'LAN_LAG_MASTER' in native_range_dest_type:
                            lag_min_links = '1'
                        cur_site_entry["native_range"]["lag_min_links"] = lag_min_links
                        
                        cur_site_entry['lan_interfaces'].append({"network_ranges": [],"default_lan":True})
                    else:
                        cur_lan_interface['id'] = ni_interface_id
                        cur_lan_interface['name'] = ni_interface_name
                        cur_lan_interface['index'] = ni_index
                        cur_lan_interface['dest_type'] = lan_ni_helper_fields.get('destType', "")
                        # temporarily add subnet to interface to be used later to flag native range_range
                        cur_lan_interface['subnet'] = lan_ni_subnet
                        cur_site_entry['lan_interfaces'].append(cur_lan_interface)
                else:
                    if hasattr(args, 'verbose') and args.verbose:
                        ni_interface_name = lan_ni_helper_fields.get('interfaceName', "")
                        ni_interface_id = lan_ni_entity_data.get('id', "")
                        print(f"WARNING: Site {lan_ni_site_id} not found in snapshot data, skipping interface {ni_interface_name} ({ni_interface_id})")

            #############################################################################
            ## Process entity lookup network ranges populating by network interface id ##
            #############################################################################
            for range in entity_network_ranges:
                if hasattr(args, 'verbose') and args.verbose:
                    print(f"Processing network range: {type(range)} - {range}")
                nr_helper_fields = range.get("helperFields", {})
                nr_entity_data = range.get('entity', {})
                nr_interface_name = str(nr_helper_fields.get('interfaceName', ""))
                nr_site_id = str(nr_helper_fields.get('siteId', ""))
                range_id = nr_entity_data.get('id', "")
                            
                nr_site_entry = next((site for site in processed_data['sites'] if site['id'] == nr_site_id), None)
                if nr_site_entry:
                    nr_subnet = nr_helper_fields.get('subnet', None)
                    nr_vlan = nr_helper_fields.get('vlanTag', None)
                    nr_mdns_reflector = nr_helper_fields.get('mdnsReflector', False)
                    nr_dhcp_microsegmentation = nr_helper_fields.get('microsegmentation', False)
                    nr_interface_name = str(nr_helper_fields.get('interfaceName', ""))
                    range_name = nr_entity_data.get('name', nr_interface_name)
                    if range_name and " \\ " in range_name:
                        range_name = range_name.split(" \\ ").pop()
                    range_id = nr_entity_data.get('id', "")

                    # Only populate one or the other depending on range_type
                    nr_gateway = None
                    nr_local_ip = None

                    cur_range_type = nr_helper_fields.get('rangeType', None)
                    if cur_range_type=="ROUTED_ROUTE":
                        nr_gateway = nr_helper_fields.get('gateway', None)
                        nr_range_type="Routed"
                        nr_dhcp_type = None
                    elif cur_range_type=="DIRECT_ROUTE":
                        nr_range_type="Direct"
                        nr_local_ip = nr_helper_fields.get('gateway', None)
                        nr_dhcp_type = None
                    else:
                        nr_range_type="VLAN"
                        nr_local_ip = nr_helper_fields.get('gateway', None)
                        nr_dhcp_type = nr_helper_fields.get('dhcpType', None)
                    
                    nr_ip_range = nr_helper_fields.get('dhcpRange', None)
                    # For JSON export, only export relay_group_name (not ID) to avoid conflicts in provider
                    nr_relay_group_id = None
                    nr_relay_group_name = nr_helper_fields.get('dhcpRelayGroupName', None)
                    nr_translated_subnet = nr_helper_fields.get('translatedSubnet', None)
                    nr_internet_only = nr_helper_fields.get('internetOnly', None)  # Default to None for JSON

                    site_native_range = nr_site_entry.get('native_range', {})                
                    if site_native_range.get("interface_name", "") == nr_interface_name:
                        if range_name!="Native Range":
                            nr_lan_interface_entry = next((lan_nic for lan_nic in nr_site_entry["lan_interfaces"] if 'default_lan' in lan_nic and lan_nic['default_lan']), None)
                            # print(f"checking range: {network_range_site_id} - {network_range_interface_name}")
                            if nr_lan_interface_entry:
                                cur_range = {}
                                cur_range['id'] = range_id
                                cur_range['name'] = range_name
                                cur_range['subnet'] = nr_subnet
                                cur_range['vlan'] = nr_vlan
                                cur_range['mdns_reflector'] = nr_mdns_reflector
                                ## The folliowing fields are missing from the schema, populating blank fields in the interim
                                cur_range['gateway'] = nr_gateway
                                cur_range['range_type'] = nr_range_type
                                cur_range['translated_subnet'] = None if nr_translated_subnet == cur_range['subnet'] else nr_translated_subnet
                                cur_range['internet_only'] = nr_internet_only
                                cur_range['local_ip'] = nr_local_ip
                                # For default_lan, use the native range's index instead of the lan_interface entry
                                cur_range['interface_index'] = site_native_range.get('index', '')  # Add interface_index for proper association
                                cur_range['dhcp_settings'] = {
                                    'dhcp_type': nr_dhcp_type,
                                    'ip_range': nr_ip_range,
                                    'relay_group_id': nr_relay_group_id,
                                    'relay_group_name': nr_relay_group_name,
                                    'dhcp_microsegmentation': nr_dhcp_microsegmentation
                                }
                                nr_lan_interface_entry["network_ranges"].append(cur_range)
                        else:
                            site_native_range['range_name'] = range_name
                            site_native_range['range_id'] = range_id
                            site_native_range['vlan'] = nr_vlan
                            site_native_range['mdns_reflector'] = nr_mdns_reflector
                            # site_native_range['dhcp_microsegmentation'] = nr_dhcp_microsegmentation
                            site_native_range['gateway'] = nr_gateway
                            site_native_range['range_type'] = nr_range_type
                            site_native_range['translated_subnet'] = None if nr_translated_subnet == site_native_range['subnet'] else nr_translated_subnet
                            site_native_range['internet_only'] = nr_internet_only
                            site_native_range['local_ip'] = nr_local_ip
                            site_native_range['dhcp_settings'] = {
                                'dhcp_type': nr_dhcp_type,
                                'ip_range': nr_ip_range,
                                'relay_group_id': nr_relay_group_id,
                                'relay_group_name': nr_relay_group_name,
                                'dhcp_microsegmentation': nr_dhcp_microsegmentation
                            }
                    else:
                        nr_lan_interface_entry = next((lan_nic for lan_nic in nr_site_entry["lan_interfaces"] if ('default_lan' not in lan_nic or not lan_nic['default_lan']) and lan_nic['name'] == nr_interface_name), None)
                        if nr_lan_interface_entry:
                            cur_range = {}
                            cur_range['id'] = range_id
                            cur_range['name'] = range_name
                            cur_range['subnet'] = nr_subnet
                            cur_range['vlan'] = nr_vlan
                            cur_range['mdns_reflector'] = nr_mdns_reflector
                            ## The folliowing fields are missing from the schema, populating blank fields in the interim
                            cur_range['gateway'] = nr_gateway
                            cur_range['range_type'] = nr_range_type
                            cur_range['translated_subnet'] = None if nr_translated_subnet == cur_range['subnet'] else nr_translated_subnet
                            cur_range['internet_only'] = nr_internet_only
                            cur_range['local_ip'] = nr_local_ip  # Use the calculated or original value
                            # For regular LAN interfaces, use the LAN interface's own index
                            cur_range['interface_index'] = nr_lan_interface_entry.get('index', '')  # Add interface_index for proper association
                            cur_range['dhcp_settings'] = {
                                'dhcp_type': nr_dhcp_type,
                                'ip_range': nr_ip_range,
                                'relay_group_id': nr_relay_group_id,
                                'relay_group_name': nr_relay_group_name,
                                'dhcp_microsegmentation': nr_dhcp_microsegmentation
                            }
                            # DEBUG
                            # print(json.dumps(nr_lan_interface_entry,indent=4,sort_keys=True))
                            # print("nr_subnet",nr_subnet)
                            # print('nr_lan_interface_entry["subnet"]='+nr_lan_interface_entry["subnet"])
                            # print(json.dumps(nr_lan_interface_entry,indent=4,sort_keys=True))
                            if "subnet" in nr_lan_interface_entry and nr_subnet==nr_lan_interface_entry["subnet"]:
                                cur_range['native_range'] = True
                                del nr_lan_interface_entry["subnet"]

                            nr_lan_interface_entry["network_ranges"].append(cur_range)
                        else:
                            if hasattr(args, 'verbose') and args.verbose:
                                print(f"Skipping range {nr_entity_data.get('id', '')}: site_id {nr_site_id} and {nr_interface_name} not found in ")
            else:
                if hasattr(args, 'verbose') and args.verbose:
                    print(f"Skipping range, site_id is unsupported for export {nr_site_id}")
        
        # Prune attributes from json        
        for site in processed_data['sites']:
            if '_interface_lookup' in site:
                del site['_interface_lookup']

        # Handle custom filename and timestamp
        if hasattr(args, 'json_filename') and args.json_filename:
            # User provided custom filename
            base_filename = args.json_filename
            # Remove .json extension if provided, we'll add it back
            if base_filename.endswith('.json'):
                base_filename = base_filename[:-5]
            
            if hasattr(args, 'append_timestamp') and args.append_timestamp:
                timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
                filename_template = f"{base_filename}_{timestamp}.json"
            else:
                filename_template = f"{base_filename}.json"
        else:
            # Use default filename template
            if hasattr(args, 'append_timestamp') and args.append_timestamp:
                timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
                filename_template = f"socket_sites_{{account_id}}_{timestamp}.json"
            else:
                filename_template = "socket_sites_{account_id}.json"
        
        if hasattr(args, 'verbose') and args.verbose:
            if hasattr(args, 'json_filename') and args.json_filename:
                print(f"Using custom filename template: {filename_template}")
            else:
                print(f"Using default filename template: {filename_template}")
            
        # Write the processed data to file using the general-purpose function
        print(f"\nWriting data to file...")
        output_file = writeDataToFile(
            data=processed_data,
            args=args,
            account_id=account_id,
            default_filename_template=filename_template,
            default_directory="config_data"
        )
        
        print(f"\n Export completed successfully!")
        print(f"  Output file: {output_file}")
        print(f"  Total sites exported: {len(processed_data['sites'])}")
        
        return [{"success": True, "output_file": output_file, "account_id": account_id}]
            
    except Exception as e:
        # Get the current exception info
        exc_type, exc_value, exc_traceback = sys.exc_info()
        
        # Get the line number where the error occurred
        line_number = exc_traceback.tb_lineno
        filename = exc_traceback.tb_frame.f_code.co_filename
        function_name = exc_traceback.tb_frame.f_code.co_name
        
        # Get the full traceback as a string
        full_traceback = traceback.format_exc()
        
        # Create detailed error message
        error_details = {
            "error_type": exc_type.__name__,
            "error_message": str(exc_value),
            "line_number": line_number,
            "function_name": function_name,
            "filename": os.path.basename(filename),
            "full_traceback": full_traceback
        }
        
        # Print detailed error information
        print(f"ERROR: {exc_type.__name__}: {str(exc_value)}")
        print(f"Location: {os.path.basename(filename)}:{line_number} in {function_name}()")
        print(f"Full traceback:\n{full_traceback}")
        
        return [{"success": False, "error": str(e), "error_details": error_details}]


def export_socket_site_to_csv(args, configuration):
    """
    Export consolidated site and socket data to CSV format
    Creates main sites CSV and individual network ranges CSV files
    """
    try:
        # Get the processed data directly without creating JSON file
        processed_data = get_processed_site_data(args, configuration)
        
        if not processed_data or not processed_data.get('sites'):
            account_id = getAccountID(args, configuration)
            return [{"success": False, "error": "No sites data found to export", "account_id": account_id}]
        if hasattr(args, 'verbose') and args.verbose:
            print(f"DEBUG: processed_data "+json.dumps(processed_data, indent=4,sort_keys=True))


        account_id = getAccountID(args, configuration)
        output_files = []
        
        # Export main sites CSV
        print(f"\nExporting main sites CSV...")
        sites_csv_file = export_sites_to_csv(processed_data['sites'], args, account_id)
        output_files.append(sites_csv_file)
        
        # Export individual network ranges CSV files for each site
        total_sites = len(processed_data['sites'])
        print(f"\nExporting network ranges CSV files for {total_sites} Cato physical socket site(s)...")
        for idx, site in enumerate(processed_data['sites'], 1):
            site_name = site.get('name', 'Unknown')
            print(f"[{idx}/{total_sites}] Exporting network ranges CSV for: {site_name}")
            ranges_csv_file = export_network_ranges_to_csv(site, args, account_id)
            if ranges_csv_file:
                output_files.append(ranges_csv_file)
        
        print(f"\n CSV export completed successfully!")
        print(f"  Total files created: {len(output_files)}")
        
        return [{"success": True, "output_files": output_files, "account_id": account_id}]
        
    except Exception as e:
        exc_type, exc_value, exc_traceback = sys.exc_info()
        line_number = exc_traceback.tb_lineno
        filename = exc_traceback.tb_frame.f_code.co_filename
        function_name = exc_traceback.tb_frame.f_code.co_name
        full_traceback = traceback.format_exc()
        
        error_details = {
            "error_type": exc_type.__name__,
            "error_message": str(exc_value),
            "line_number": line_number,
            "function_name": function_name,
            "filename": os.path.basename(filename),
            "full_traceback": full_traceback
        }
        
        print(f"ERROR: {exc_type.__name__}: {str(exc_value)}")
        print(f"Location: {os.path.basename(filename)}:{line_number} in {function_name}()")
        print(f"Full traceback:\n{full_traceback}")
        
        return [{"success": False, "error": str(e), "error_details": error_details}]


def get_processed_site_data(args, configuration):
    """
    Get processed site data without writing to JSON file
    Reuses the logic from export_socket_site_to_json but returns data instead
    """
    processed_data = {'sites': []}
    
    # Load CLI settings
    settings = load_cli_settings()
    # Note: load_cli_settings() now returns embedded defaults if file cannot be loaded
    
    account_id = getAccountID(args, configuration)
    
    # Get siteIDs from args if provided
    site_ids = []
    if hasattr(args, 'siteIDs') and args.siteIDs:
        site_ids = [site_id.strip() for site_id in args.siteIDs.split(',') if site_id.strip()]
    
    if hasattr(args, 'verbose') and args.verbose:
        if site_ids:
            print(f"Found {len(site_ids)} site(s) matching the provided site IDs")
        else:
            print(f"Found {len(site_ids)} site(s) in account")

    if not site_ids:
        site_ids = None
    
    ###############################################################
    ## Call APIs to retrieve sites, interface and network ranges ##
    ###############################################################
    entity_sites = getEntityLookup(args, configuration, account_id, "site", site_ids, None)
    if not entity_sites or len(entity_sites) == 0:
        if site_ids:
            # User provided specific site IDs but none were found
            print(f"No sites found matching the provided site IDs: {', '.join(site_ids)}")
            print("Please verify the site IDs are correct and that they exist in this account.")
        else:
            # No site filter was provided but no sites exist at all
            print("No sites found in this account.")
        return None
    
    total_sites = len(entity_sites)
    print(f"\nExporting Cato physical socket sites:\n")
    print(f"{total_sites} site(s) found...\n")

    for idx, entity_site in enumerate(entity_sites, 1):
        site_id = entity_site["entity"]["id"]
        entity_input = {"id":site_id,"type":"site"}
        snapshot_response = getAccountSnapshot(args, configuration, account_id, [site_id])
        snapshot_sites = snapshot_response['data']['accountSnapshot']['sites']
        snapshot_site = snapshot_sites[0] if snapshot_sites else {}
        entity_network_interfaces = getEntityLookup(args, configuration, account_id, "networkInterface",None, entity_input)
        entity_network_ranges = getEntityLookup(args, configuration, account_id, "siteRange",None, entity_input)

        connectionType = snapshot_site.get('infoSiteSnapshot', {}).get('connType', "")
        site_name = snapshot_site.get('infoSiteSnapshot', {}).get('name', 'Unknown')
                
        cur_site = {
            'wan_interfaces': [],
            'lan_interfaces': [],
            'native_range': {}
        }
        
        if connectionType not in settings["export_by_socket_type"]:
            print(f"[{idx}/{total_sites}] Ignoring cloud site: {site_name} (ID: {site_id})")
        else:
            print(f"[{idx}/{total_sites}] Processing physical socket site: {site_name} (ID: {site_id})")

            cur_site['id'] = site_id
            cur_site['name'] = snapshot_site.get('infoSiteSnapshot', {}).get('name')
            cur_site['description'] = snapshot_site.get('infoSiteSnapshot', {}).get('description')
            cur_site['connection_type'] = connectionType
            cur_site['type'] = snapshot_site.get('infoSiteSnapshot', {}).get('type')
            cur_site = populateSiteLocationData(args, snapshot_site, cur_site)
            
            # Create a map of interfaces from account snapshot for native range lookup
            site_interfaces = snapshot_site.get('infoSiteSnapshot', {}).get('interfaces', [])
            interface_lookup = {}  # Map interface ID to interface data

            # Build precedence map from devices.interfaces.naturalOrder
            # naturalOrder: 1 = ACTIVE, 2 = PASSIVE, 3 = LAST_RESORT
            precedence_map = {}  # Map interface ID to precedence string
            site_devices = snapshot_site.get('devices') or []
            for device in site_devices:
                device_interfaces = device.get('interfaces', [])
                for device_iface in device_interfaces:
                    iface_id = device_iface.get('id')
                    natural_order = device_iface.get('naturalOrder')
                    if iface_id and natural_order:
                        if natural_order == 1:
                            precedence_map[iface_id] = "ACTIVE"
                        elif natural_order == 2:
                            precedence_map[iface_id] = "PASSIVE"
                        elif natural_order == 3:
                            precedence_map[iface_id] = "LAST_RESORT"

            if hasattr(args, 'verbose') and args.verbose:
                print(f"DEBUG: Processing site {site_id} ({cur_site['name']}) with {len(site_interfaces)} interfaces")
                print(f"DEBUG: Built precedence map with {len(precedence_map)} entries: {precedence_map}")

            for interface in site_interfaces:
                role = interface.get('wanRoleInterfaceInfo', "")
                dest_type = interface.get('destType', "")
                interfaceName = interface.get('id', "")
                
                # Store interface data for lookup
                interface_lookup[interfaceName] = {
                    'dest_type': dest_type,
                    'name': interface.get('name', ""),
                    'role': role
                }
                
                # Process WAN interfaces
                if role is not None and role[0:3] == "wan":
                    cur_wan_interface = {}
                    if interfaceName[0:3] in ("WAN", "USB", "LTE"):
                        cur_wan_interface['id'] = site_id+":"+ interface.get('id', "")
                    else:
                        cur_wan_interface['id'] = site_id+":INT_"+ interface.get('id', "")
                    # Format WAN interface index: INT_X for numeric values, keep as-is for non-numeric
                    wan_interface_id = interface.get('id', "")
                    if isinstance(wan_interface_id, (int, str)) and str(wan_interface_id).isdigit():
                        cur_wan_interface['index'] = f"INT_{wan_interface_id}"
                    else:
                        cur_wan_interface['index'] = wan_interface_id
                    cur_wan_interface['name'] = interface.get('name', "")
                    cur_wan_interface['upstream_bandwidth'] = interface.get('upstreamBandwidth', 0)
                    cur_wan_interface['downstream_bandwidth'] = interface.get('downstreamBandwidth', 0)
                    cur_wan_interface['dest_type'] = dest_type
                    cur_wan_interface['role'] = role
                    # Get precedence from naturalOrder mapping, default to ACTIVE if not found
                    cur_wan_interface['precedence'] = precedence_map.get(wan_interface_id, "ACTIVE")
                    cur_site['wan_interfaces'].append(cur_wan_interface)
                
                # Process LAN_LAG_MEMBER interfaces
                elif dest_type == "LAN_LAG_MEMBER":
                    if hasattr(args, 'verbose') and args.verbose:            
                        print(f"DEBUG: Processing LAN_LAG_MEMBER interface for site {site_id}: {interface.get('name', '')} (id: {interface.get('id', '')})")
                    cur_lan_interface = {
                        'network_ranges': []
                    }
                    # LAN_LAG_MEMBER interfaces don't have a numeric ID, only index and name
                    cur_lan_interface['id'] = ''  # No ID for LAN_LAG_MEMBER
                    cur_lan_interface['name'] = interface.get('name', "")
                    # Format interface index: INT_X for numeric values, keep as-is for non-numeric
                    interface_id = interface.get('id', "")
                    if isinstance(interface_id, (int, str)) and str(interface_id).isdigit():
                        cur_lan_interface['index'] = f"INT_{interface_id}"
                    else:
                        cur_lan_interface['index'] = interface_id
                    cur_lan_interface['dest_type'] = dest_type
                    cur_site['lan_interfaces'].append(cur_lan_interface)
                    if hasattr(args, 'verbose') and args.verbose:
                        print(f"DEBUG: Added LAN_LAG_MEMBER interface: {cur_lan_interface}")
            
            # Store the interface lookup for later use
            cur_site['_interface_lookup'] = interface_lookup
            
            if site_id:
                processed_data['sites'].append(cur_site)
                if hasattr(args, 'verbose') and args.verbose:        
                    print(f"DEBUG: Added site {site_id} ({cur_site['name']}) with {len(cur_site['lan_interfaces'])} LAN interfaces")
        
        ##################################################################################
        ## Process entity lookup LAN network interfaces adding to site object by site_id##
        ##################################################################################
        for lan_ni in entity_network_interfaces:
            lan_ni_helper_fields = lan_ni.get("helperFields", {})
            lan_ni_entity_data = lan_ni.get('entity', {})
            lan_ni_site_id = str(lan_ni_helper_fields.get('siteId', ""))
            cur_site_entry = next((site for site in processed_data['sites'] if site['id'] == lan_ni_site_id), None)
            if cur_site_entry:
                cur_lan_interface = {'network_ranges': []}
                ni_interface_id = lan_ni_entity_data.get('id', "")
                ni_interface_name = lan_ni_helper_fields.get('interfaceName', "")
                lan_ni_subnet = str(lan_ni_helper_fields.get('subnet', ""))
                ni_index = lan_ni_helper_fields.get('interfaceId', "")
                ni_index = f"INT_{ni_index}" if isinstance(ni_index, (int, str)) and str(ni_index).isdigit() else ni_index
                # Use isDefault flag from API instead of hardcoded map
                is_default_interface = lan_ni_helper_fields.get('isDefault', False)
                
                if is_default_interface:
                    cur_site_entry["native_range"]["interface_id"] = ni_interface_id
                    cur_site_entry["native_range"]["interface_name"] = ni_interface_name
                    cur_site_entry["native_range"]["subnet"] = lan_ni_subnet
                    cur_site_entry["native_range"]["index"] = ni_index
                    
                    # Get interface details from the stored lookup data
                    interface_lookup = cur_site_entry.get('_interface_lookup', {})
                    interface_details = interface_lookup.get(str(lan_ni_helper_fields.get('interfaceId', '')), {})
                    native_range_dest_type = interface_details.get('dest_type', '')
                    cur_site_entry["native_range"]["dest_type"] = native_range_dest_type
                    
                    # Set lag_min_links to 1 only for LAN_LAG_MASTER interfaces
                    lag_min_links = ''
                    if 'LAN_LAG_MASTER' in native_range_dest_type:
                        lag_min_links = '1'
                    cur_site_entry["native_range"]["lag_min_links"] = lag_min_links
                    
                    cur_site_entry['lan_interfaces'].append({"network_ranges": [], "default_lan": True})
                else:
                    cur_lan_interface['id'] = ni_interface_id
                    cur_lan_interface['name'] = ni_interface_name
                    cur_lan_interface['index'] = ni_index
                    cur_lan_interface['dest_type'] = lan_ni_helper_fields.get('destType', "")
                    # temporarily add subnet to interface to be used later to flag native range_range
                    cur_lan_interface['subnet'] = lan_ni_subnet
                    cur_site_entry['lan_interfaces'].append(cur_lan_interface)
            else:
                if hasattr(args, 'verbose') and args.verbose:
                    ni_interface_name = lan_ni_helper_fields.get('interfaceName', "")
                    ni_interface_id = lan_ni_entity_data.get('id', "")
                    print(f"WARNING: Site {lan_ni_site_id} not found in snapshot data, skipping interface {ni_interface_name} ({ni_interface_id})")
        
        #############################################################################
        ## Process entity lookup network ranges populating by network interface id ##
        #############################################################################
        for range in entity_network_ranges:
            nr_helper_fields = range.get("helperFields", {})
            nr_entity_data = range.get('entity', {})
            nr_interface_name = str(nr_helper_fields.get('interfaceName', ""))
            nr_site_id = str(nr_helper_fields.get('siteId', ""))
            range_id = nr_entity_data.get('id', "")
            
            nr_site_entry = next((site for site in processed_data['sites'] if site['id'] == nr_site_id), None)
                        
            if nr_site_entry:
                if hasattr(args, 'verbose') and args.verbose:
                    print(f"DEBUG: nr_site_entry[native_range] "+json.dumps(nr_site_entry["native_range"],indent=2))
                    print(f"DEBUG: range {json.dumps(range,indent=2)}")
                nr_subnet = nr_helper_fields.get('subnet', None)
                nr_vlan = nr_helper_fields.get('vlanTag', None)
                nr_mdns_reflector = nr_helper_fields.get('mdnsReflector', False)
                nr_dhcp_microsegmentation = nr_helper_fields.get('microsegmentation', False)
                range_name = nr_entity_data.get('name', nr_interface_name)
                if range_name and " \\ " in range_name:
                    range_name = range_name.split(" \\ ").pop()
                
                # Only populate one or the other depending on range_type
                nr_gateway = None
                nr_local_ip = None

                cur_range_type = nr_helper_fields.get('rangeType', None)
                if cur_range_type=="ROUTED_ROUTE":
                    nr_gateway = nr_helper_fields.get('gateway', None)
                    nr_range_type="Routed"
                    nr_dhcp_type = None
                elif cur_range_type=="DIRECT_ROUTE":
                    nr_range_type="Direct"
                    nr_local_ip = nr_helper_fields.get('gateway', None)
                    nr_dhcp_type = None
                else:
                    nr_range_type="VLAN"
                    nr_local_ip = nr_helper_fields.get('gateway', None)
                    nr_dhcp_type = nr_helper_fields.get('dhcpType', None)
                
                nr_ip_range = nr_helper_fields.get('dhcpRange', None)
                # nr_relay_group_id = nr_helper_fields.get('dhcpRelayGroupId', None)
                nr_relay_group_id = None # Leave as None only exporting the name
                nr_relay_group_name = nr_helper_fields.get('dhcpRelayGroupName', None)
                nr_translated_subnet = nr_helper_fields.get('translatedSubnet', None)
                nr_internet_only = nr_helper_fields.get('internetOnly', None)  # Default to None for JSON

                site_native_range = nr_site_entry.get('native_range', {})
                if hasattr(args, 'verbose') and args.verbose:        
                    print(f"DEBUG: nr_subnet={nr_subnet}, nr_interface_name={nr_interface_name}, range_name={range_name}")
                    print(f"DEBUG: nr_subnet={nr_subnet}, nr_interface_name={nr_interface_name}, range_name={range_name}")
                    print(f"DEBUG: entity_network_ranges.nr_helper_fields="+json.dumps(nr_helper_fields,indent=2))
                    print(f"DEBUG: dhcpType=",nr_helper_fields.get('dhcpType',None))
                    print(f"DEBUG: dhcpRange=",nr_helper_fields.get('dhcpRange',None))
                    print(f"DEBUG: dhcpRelayGroupName=",nr_helper_fields.get('dhcpRelayGroupName',None))
                    print(f"DEBUG: translatedSubnet=",nr_helper_fields.get('translatedSubnet',None))

                if site_native_range.get("interface_name", "") == nr_interface_name:
                    # site_native_range["vlan"] = "vlantest111"
                    if range_name != "Native Range":
                        # nr_site_entry["native_range"]["vlan"] = "vlantest12"
                        if hasattr(args, 'verbose') and args.verbose:
                            print(f"DEBUG: Processing nr_subnet==subnet")
                            # print(f"DEBUG: Processing interface_name==nr_interface_name - '{range_name}' on interface '{nr_interface_name}'")
                            # print(f"DEBUG: Range subnet: {nr_subnet}, Native range subnet: {site_native_range.get('subnet', '')}")
                            # print(f"DEBUG: Native range interface_name: '{site_native_range.get('interface_name', '')}'")
                            # print(f"DEBUG: Match: {site_native_range.get('interface_name', '') == nr_interface_name}")
                        nr_lan_interface_entry = next((lan_nic for lan_nic in nr_site_entry["lan_interfaces"] if 'default_lan' in lan_nic and lan_nic['default_lan']), None)
                        if hasattr(args, 'verbose') and args.verbose:
                            print("DEBUG: nr_lan_interface_entry ",nr_lan_interface_entry)
                        if nr_lan_interface_entry:
                            cur_range = {
                                'id': range_id, 
                                'name': range_name, 
                                'subnet': nr_subnet, 
                                'vlan': nr_vlan,
                                'mdns_reflector': nr_mdns_reflector, 
                                'gateway': nr_gateway, 
                                'range_type': nr_range_type,
                                'translated_subnet': None if nr_translated_subnet == nr_subnet else nr_translated_subnet, 
                                'internet_only': nr_internet_only, 
                                'local_ip': nr_local_ip,
                                'dhcp_settings': {
                                    'dhcp_type': nr_dhcp_type, 
                                    'ip_range': nr_ip_range,
                                     # 'relay_group_id': nr_relay_group_id,
                                     'relay_group_id': None, # Leave none and use the name
                                    'relay_group_name': nr_relay_group_name, 
                                    'dhcp_microsegmentation': nr_dhcp_microsegmentation
                                }
                            }
                            nr_lan_interface_entry["network_ranges"].append(cur_range)
                    else:
                        if hasattr(args, 'verbose') and args.verbose:
                            print(f"DEBUG: Processing ELSE.nr_subnet==subnet")
                            print(f"DEBUG: Populating native_range for site {nr_site_entry.get('name', '')}")
                            print(f"DEBUG: vlan={nr_vlan}, gateway={nr_gateway}, range_type={nr_range_type}")
                            print(f"DEBUG: translated_subnet={nr_translated_subnet}, mdns_reflector={nr_mdns_reflector}")
                        
                        site_native_range['range_name'] = range_name
                        site_native_range['range_id'] = range_id
                        site_native_range['vlan'] = nr_vlan
                        site_native_range['mdns_reflector'] = nr_mdns_reflector
                        # site_native_range['gateway'] = nr_gateway
                        site_native_range['range_type'] = nr_range_type
                        site_native_range['translated_subnet'] = None if nr_translated_subnet == site_native_range['subnet'] else nr_translated_subnet
                        # site_native_range['internet_only'] = nr_internet_only
                        site_native_range['local_ip'] = nr_local_ip
                        site_native_range['dhcp_settings'] = {
                            'dhcp_type': nr_dhcp_type, 
                            'ip_range': nr_ip_range, 
                            'relay_group_id': nr_relay_group_id,
                            'relay_group_name': nr_relay_group_name, 
                            'dhcp_microsegmentation': nr_dhcp_microsegmentation
                        }
                        
                        if hasattr(args, 'verbose') and args.verbose:
                            print(f"DEBUG: After setting, site native_range keys: {list(nr_site_entry['native_range'].keys())}")
                            print(f"DEBUG: site native_range vlan: {nr_site_entry['native_range'].get('vlan', 'NOT SET')}")
                            print(f"DEBUG: site_native_range {json.dumps(site_native_range,indent=2)}")
                else:
                    if hasattr(args, 'verbose') and args.verbose:
                        print(f"DEBUG: Processing ELSE.interface_name==nr_interface_name - '{range_name}' on interface '{nr_interface_name}'")

                    nr_lan_interface_entry = next((lan_nic for lan_nic in nr_site_entry["lan_interfaces"] if ('default_lan' not in lan_nic or not lan_nic['default_lan']) and lan_nic['name'] == nr_interface_name), None)
                    if nr_lan_interface_entry:
                        cur_range = {
                            'id': range_id, 
                            'name': range_name, 
                            'subnet': nr_subnet, 
                            'vlan': nr_vlan,
                            'mdns_reflector': nr_mdns_reflector, 
                            'gateway': nr_gateway, 
                            'range_type': nr_range_type,
                            'translated_subnet': None if nr_translated_subnet == nr_subnet else nr_translated_subnet, 
                            'internet_only': nr_internet_only, 
                            'local_ip': nr_local_ip,
                            'dhcp_settings': {
                                'dhcp_type': nr_dhcp_type, 
                                'ip_range': nr_ip_range, 
                                'relay_group_id': nr_relay_group_id,
                                'relay_group_name': nr_relay_group_name, 
                                'dhcp_microsegmentation': nr_dhcp_microsegmentation
                            }
                        }
                        
                        if "subnet" in nr_lan_interface_entry and nr_subnet == nr_lan_interface_entry["subnet"]:
                            cur_range['native_range'] = True
                            del nr_lan_interface_entry["subnet"]

                        if hasattr(args, 'verbose') and args.verbose:
                            print(f"DEBUG: nr_lan_interface_entry=True")
                            print(f"DEBUG: nr_helper_fields: {json.dumps(nr_helper_fields, indent=2, sort_keys=True)}")
                            print(f"DEBUG: nr_entity_data: {json.dumps(nr_entity_data, indent=2, sort_keys=True)}")
                            print(f"DEBUG: nr_vlan: {nr_vlan}")
                            print(f"DEBUG: cur_range: {json.dumps(cur_range, indent=2, sort_keys=True)}")        
                            print(f"DEBUG: Range subnet: {nr_subnet}, Native range subnet: {site_native_range.get('subnet', '')}")
                            print(f"DEBUG: Native range interface_name: '{site_native_range.get('interface_name', '')}'")
                            print(f"DEBUG: Match: {site_native_range.get('interface_name', '') == nr_interface_name}")
                            print(f"DEBUG: nr_helper_fields "+json.dumps(nr_helper_fields,indent=2,sort_keys=True))
                            print(f"DEBUG: nr_entity_data "+json.dumps(nr_entity_data,indent=2,sort_keys=True))        

                        nr_lan_interface_entry["network_ranges"].append(cur_range)
        
    return processed_data


def export_sites_to_csv(sites, args, account_id):
    """
    Export main sites data to CSV file in bulk-sites format
    One row per WAN interface, site attributes only on first row per site
    """
    # Handle custom filename and timestamp
    if hasattr(args, 'csv_filename') and args.csv_filename:
        base_filename = args.csv_filename
        if base_filename.endswith('.csv'):
            base_filename = base_filename[:-4]
        
        if hasattr(args, 'append_timestamp') and args.append_timestamp:
            timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
            filename_template = f"{base_filename}_{timestamp}.csv"
        else:
            filename_template = f"{base_filename}.csv"
    else:
        if hasattr(args, 'append_timestamp') and args.append_timestamp:
            timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
            filename_template = f"socket_sites_{{account_id}}_{timestamp}.csv"
        else:
            filename_template = "socket_sites_{account_id}.csv"
    
    # Replace account_id placeholder
    filename = filename_template.format(account_id=account_id)
    
    # Determine output directory
    output_dir = getattr(args, 'output_directory', None)
    if not output_dir:
        output_dir = 'config_data'
    if not os.path.isabs(output_dir):
        output_dir = os.path.join(os.getcwd(), output_dir)
    
    os.makedirs(output_dir, exist_ok=True)
    filepath = os.path.join(output_dir, filename)
    
    # Define CSV headers matching bulk-sites.csv format
    headers = [
        'site_id',
        'site_name',
        'wan_interface_id',
        'wan_interface_index',
        'wan_interface_name', 
        'wan_upstream_bw',
        'wan_downstream_bw',
        'wan_role',
        'wan_precedence',
        'site_type',
        'connection_type',
        'site_description',
        'native_range_id',
        'native_range_subnet',
        'native_range_local_ip',
        'native_range_translated_subnet',
        'native_range_vlan',
        'native_range_mdns_reflector',
        'native_range_type',
        'native_range_interface_id',
        'native_range_interface_index',
        'native_range_interface_name',
        'native_range_interface_dest_type',
        'native_range_interface_lag_min_links',
        'native_range_dhcp_type',
        'native_range_dhcp_ip_range',
        'native_range_dhcp_relay_group_id',
        'native_range_dhcp_relay_group_name',
        'native_range_dhcp_microsegmentation',
        'site_location_address',
        'site_location_city',
        'site_location_country_code',
        'site_location_state_code',
        'site_location_timezone',
        'license_id',
        'license_bw'
    ]
    
    rows = []
    total_interfaces = 0
    
    for site in sites:
        site_name = site.get('name', '')
        wan_interfaces = site.get('wan_interfaces', [])
        
        # Sort WAN interfaces to ensure 'wan_1' role appears first
        # This organizes the CSV so wan_1 interfaces are on the first line for each site
        def wan_interface_sort_key(interface):
            role = interface.get('role', '').lower()
            if role == 'wan_1':
                return 0  # wan_1 comes first
            elif role == 'wan_2':
                return 1
            elif role == 'wan_3':
                return 2
            elif role == 'wan_4':
                return 3
            else:
                return 9  # Unknown/other roles come last
        
        wan_interfaces.sort(key=wan_interface_sort_key)
        
        # If no WAN interfaces, create one empty row for the site
        if not wan_interfaces:
            wan_interfaces = [{}]  # Empty interface to ensure site is included
        
        for idx, wan_interface in enumerate(wan_interfaces):
            # Site-specific attributes only on first row per site
            is_first_interface = (idx == 0)
            
            # Calculate local IP from native range subnet if available
            native_range = site.get('native_range', {})
            native_subnet = native_range.get('subnet', '')
            
            if hasattr(args, 'verbose') and args.verbose and is_first_interface:
                print(f"\nDEBUG CSV: Site '{site_name}' native_range keys: {list(native_range.keys())}")
                print(f"DEBUG CSV: vlan={native_range.get('vlan', 'NOT SET')}")
                print(f"DEBUG CSV: gateway={native_range.get('gateway', 'NOT SET')}")
                print(f"DEBUG CSV: range_type={native_range.get('range_type', 'NOT SET')}")
                print(f"DEBUG CSV: translated_subnet={native_range.get('translated_subnet', 'NOT SET')}")
                print(f"DEBUG CSV: mdns_reflector={native_range.get('mdns_reflector', 'NOT SET')}")
            
            row = {
                'site_id': site.get('id', '') if is_first_interface else '',
                'site_name': site_name,
                'wan_interface_id': wan_interface.get('id', '') if wan_interface else '',
                'wan_interface_index': wan_interface.get('index', '') if wan_interface else '',
                'wan_interface_name': wan_interface.get('name', '') if wan_interface else '',
                'wan_upstream_bw': wan_interface.get('upstream_bandwidth', '') if wan_interface else '',
                'wan_downstream_bw': wan_interface.get('downstream_bandwidth', '') if wan_interface else '',
                'wan_role': wan_interface.get('role', '') if wan_interface else '',
                'wan_precedence': wan_interface.get('precedence', '') if wan_interface else '',
                
                # Site attributes - only populate on first interface row
                'site_type': site.get('type', '') if is_first_interface else '',
                'connection_type': site.get('connection_type', '') if is_first_interface else '',
                'site_description': site.get('description', '') if is_first_interface else '',
                'native_range_id': native_range.get('range_id', '') if is_first_interface else '',
                'native_range_subnet': native_subnet if is_first_interface else '',
                'native_range_local_ip': native_range.get('local_ip', '') if is_first_interface else '',
                'native_range_translated_subnet': native_range.get('translated_subnet', '') if is_first_interface else '',
                'native_range_vlan': native_range.get('vlan', '') if is_first_interface else '',
                'native_range_mdns_reflector': str(native_range.get('mdns_reflector', '')).upper() if is_first_interface and native_range.get('mdns_reflector') != '' else '' if is_first_interface else '',
                'native_range_type': native_range.get('range_type', '') if is_first_interface else '',
                'native_range_interface_id': native_range.get('interface_id', '') if is_first_interface else '',
                'native_range_interface_index': native_range.get('index', '') if is_first_interface else '',
                'native_range_interface_name': native_range.get('interface_name', '') if is_first_interface else '',
                'native_range_interface_dest_type': native_range.get('dest_type', '') if is_first_interface else '',
                'native_range_interface_lag_min_links': native_range.get('lag_min_links', '') if is_first_interface else '',
                'native_range_dhcp_type': native_range.get('dhcp_settings', {}).get('dhcp_type', '') if is_first_interface else '',
                'native_range_dhcp_ip_range': native_range.get('dhcp_settings', {}).get('ip_range', '') if is_first_interface else '',
                'native_range_dhcp_relay_group_id': native_range.get('dhcp_settings', {}).get('relay_group_id', '') if is_first_interface else '',
                'native_range_dhcp_relay_group_name': native_range.get('dhcp_settings', {}).get('relay_group_name', '') if is_first_interface else '',
                'native_range_dhcp_microsegmentation': str(native_range.get('dhcp_settings', {}).get('dhcp_microsegmentation', '')).upper() if is_first_interface and native_range.get('dhcp_settings', {}).get('dhcp_microsegmentation') != '' else '' if is_first_interface else '',
                'site_location_address': site.get('site_location', {}).get('address', '') if is_first_interface else '',
                'site_location_city': site.get('site_location', {}).get('city', '') if is_first_interface else '',
                'site_location_country_code': site.get('site_location', {}).get('countryCode', '') if is_first_interface else '',
                'site_location_state_code': site.get('site_location', {}).get('stateCode', '') if is_first_interface else '',
                'site_location_timezone': site.get('site_location', {}).get('timezone', '') if is_first_interface else '',
                'license_id': '',
                'license_bw': ''
            }
            
            rows.append(row)
            total_interfaces += 1 if wan_interface else 0
    
    with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
        writer = csv.DictWriter(csvfile, fieldnames=headers)
        writer.writeheader()
        writer.writerows(rows)
    
    # Clean up CSV: remove empty lines and ensure proper line endings
    clean_csv_file(filepath, verbose=hasattr(args, 'verbose') and args.verbose)
    
    if hasattr(args, 'verbose') and args.verbose:
        print(f"Exported {len(sites)} sites with {total_interfaces} WAN interfaces to {filepath}")
    
    return filepath


def export_network_ranges_to_csv(site, args, account_id):
    """
    Export network ranges for a single site to CSV file
    Structure: LAN interface as parent with network ranges as children
    First row per interface contains interface details, subsequent rows contain only network range details
    """
    site_name = site.get('name', '')
    if not site_name:
        return None
    
    # Sanitize site name for filename
    safe_site_name = "".join(c for c in site_name if c.isalnum() or c in ('-', '_')).rstrip()
    
    # Determine output directory
    output_dir = getattr(args, 'output_directory', None)
    if not output_dir:
        output_dir = 'config_data'
    if not os.path.isabs(output_dir):
        output_dir = os.path.join(os.getcwd(), output_dir)
    
    # Create sites_config subdirectory with account ID
    sites_config_dir = os.path.join(output_dir, f'sites_config_{account_id}')
    os.makedirs(sites_config_dir, exist_ok=True)
    
    filename = f"{safe_site_name}_network_ranges.csv"
    filepath = os.path.join(sites_config_dir, filename)
    
    # Define CSV headers - Reordered LAN interface columns first, then network range columns
    headers = [
        # LAN Interface columns (first 3 columns, lag_min_links 4th, is_native_range 5th, lan_interface_index 6th)
        'lan_interface_id', 
        'lan_interface_name', 
        'lan_interface_dest_type', 
        'lag_min_links', 
        'is_native_range', 
        'lan_interface_index',
        # Network Range columns (populated on all rows)
        'network_range_id', 
        'network_range_name', 
        'subnet', 
        'local_ip', 
        'gateway', 
        'translated_subnet', 
        'vlan', 
        'mdns_reflector', 
        'range_type', 
        'internet_only', 
        'dhcp_type', 
        'dhcp_ip_range', 
        'dhcp_relay_group_id', 
        'dhcp_relay_group_name', 
        'dhcp_microsegmentation'
    ]
    
    rows = []
    
    # Get the native range subnet from the site to exclude it from detailed CSV
    native_range_subnet = site.get('native_range', {}).get('subnet', '')
    
    # lag_min_links is hardcoded to 1 (calculation removed)
    
    # Process default LAN interface (from native_range) - ONLY if it has additional networks
    native_range = site.get('native_range', {})
    native_range_index = native_range.get('index', '')  # The default interface index from parent CSV
    default_lan_interfaces = [lan_nic for lan_nic in site.get('lan_interfaces', []) if lan_nic.get('default_lan', False)]
    
    if default_lan_interfaces:
        for default_lan_interface in default_lan_interfaces:
            interface_id = native_range.get('interface_id', '')
            interface_name = native_range.get('interface_name', '')
            interface_index = native_range.get('index', '')  # Use actual interface index like INT_5
            interface_dest_type = 'LAN'
            
            network_ranges = default_lan_interface.get('network_ranges', [])
            
            # For default_lan interfaces, process all ranges but skip native range
            # Only process non-native ranges (additional ranges)
            if network_ranges:
                # Process all ranges and only include non-native ranges
                for idx, network_range in enumerate(network_ranges):
                    current_subnet = network_range.get('subnet', '')
                    
                    # Skip the native range since it's managed at parent level
                    if current_subnet == native_range_subnet:
                        continue
                        
                    # First row for this interface includes interface details
                    is_first_range = (idx == 0)
                    
                    # For default_lan interfaces, all ranges are additional (is_native_range=FALSE)
                    # because the native range is already defined in the parent CSV
                    
                    row = {
                        # LAN Interface details - for default LAN interfaces, don't populate interface ID, name, type since managed at parent level
                        'lan_interface_id': '',  # Empty for default LAN interfaces (managed at parent level)
                        'lan_interface_name': '',  # Empty for default LAN interfaces (managed at parent level)
                        'lan_interface_dest_type': '',  # Empty for default LAN interfaces (managed at parent level)
                        'lag_min_links': '',  # Empty for default interfaces
                        'is_native_range': '',  # Always empty for default LAN interfaces (native is managed at parent level)
                        'lan_interface_index': interface_index,  # Populated for every row
                        
                        # Network Range details (on all rows)
                        'network_range_id': network_range.get('id', ''),
                        'network_range_name': network_range.get('name', ''),
                        'subnet': network_range.get('subnet', ''),
                        'local_ip': network_range.get('local_ip', ''),
                        'gateway': network_range.get('gateway', ''),
                        'translated_subnet': network_range.get('translated_subnet', ''),
                        'vlan': network_range.get('vlan', ''),
                        'mdns_reflector': str(network_range.get('mdns_reflector', False)).upper(),
                        'range_type': network_range.get('range_type', ''),
                        'internet_only': network_range.get('internet_only', ''),
                        'dhcp_type': network_range.get('dhcp_settings', {}).get('dhcp_type', ''),
                        'dhcp_ip_range': network_range.get('dhcp_settings', {}).get('ip_range', ''),
                        'dhcp_relay_group_id': network_range.get('dhcp_settings', {}).get('relay_group_id', ''),
                        'dhcp_relay_group_name': network_range.get('dhcp_settings', {}).get('relay_group_name', ''),
                        'dhcp_microsegmentation': str(network_range.get('dhcp_settings', {}).get('dhcp_microsegmentation', False)).upper()
                    }
                    rows.append(row)
    
    # Process regular LAN interfaces and their network ranges
    for lan_interface in site.get('lan_interfaces', []):
        is_default_lan = lan_interface.get('default_lan', False)
        
        # Skip default_lan interfaces (already processed above)
        if is_default_lan:
            continue
            
        interface_id = lan_interface.get('id', '')
        interface_name = lan_interface.get('name', '')
        interface_index = lan_interface.get('index', '')
        interface_dest_type = lan_interface.get('dest_type', '')
        
        # Special handling for LAN_LAG_MEMBER interfaces
        if interface_dest_type == 'LAN_LAG_MEMBER':
            if hasattr(args, 'verbose') and args.verbose:
                print(f"DEBUG: Processing LAN_LAG_MEMBER interface in CSV export for site {site.get('name', '')}: {interface_name} (index: {interface_index})")
            # LAN_LAG_MEMBER interfaces get their own row with only interface details
            # They don't have is_native_range=TRUE and have no network range data
            row = {
                # LAN Interface details - only populate specific fields for LAN_LAG_MEMBER
                'lan_interface_id': '',  # LAN_LAG_MEMBER interfaces don't have IDs
                'lan_interface_name': interface_name,
                'lan_interface_dest_type': interface_dest_type,
                'lag_min_links': '',  # Empty for LAN_LAG_MEMBER
                'is_native_range': '',  # Empty - LAN_LAG_MEMBER should NOT have is_native_range=TRUE
                'lan_interface_index': interface_index,
                # Network Range details (all empty for LAN_LAG_MEMBER)
                'network_range_id': '',
                'network_range_name': '',
                'subnet': '',
                'local_ip': '',
                'gateway': '',
                'translated_subnet': '',
                'vlan': '',
                'mdns_reflector': '',
                'range_type': '',
                'internet_only': '',
                'dhcp_type': '',
                'dhcp_ip_range': '',
                'dhcp_relay_group_id': '',
                'dhcp_relay_group_name': '',
                'dhcp_microsegmentation': ''
            }
            rows.append(row)
            if hasattr(args, 'verbose') and args.verbose:
                print(f"DEBUG: Added LAN_LAG_MEMBER row to CSV: {row}")
            continue  # Skip to next interface
        
        # Handle regular (non-LAN_LAG_MEMBER) LAN interfaces
        network_ranges = lan_interface.get('network_ranges', [])
        
        # If no network ranges at all, create at least one row with just the LAN interface info  
        # For regular LAN interfaces, mark as native range if this is the first/only interface
        if not network_ranges:
            # Set lag_min_links to 1 only for LAN_LAG_MASTER interfaces
            lag_min_links_value = ''
            if 'LAN_LAG_MASTER' in interface_dest_type:
                lag_min_links_value = '1'
            
            row = {
                # LAN Interface details - first 3 columns reordered, lag_min_links 4th, is_native_range 5th, lan_interface_index 6th
                'lan_interface_id': interface_id,
                'lan_interface_name': interface_name,
                'lan_interface_dest_type': interface_dest_type,
                'lag_min_links': lag_min_links_value,  # Populated only for LAN_LAG_MASTER
                'is_native_range': 'TRUE',  # Mark as native range for non-default LAN interfaces with no additional ranges
                'lan_interface_index': interface_index,
                
                # Network Range details (empty since no additional ranges)
                'network_range_id': '',
                'network_range_name': '',
                'subnet': '',
                'local_ip': '',
                'gateway': '',
                'translated_subnet': '',
                'vlan': '',
                'mdns_reflector': '',
                'range_type': '',
                'internet_only': '',
                'dhcp_type': '',
                'dhcp_ip_range': '',
                'dhcp_relay_group_id': '',
                'dhcp_relay_group_name': '',
                'dhcp_microsegmentation': ''
            }
            rows.append(row)
        else:
            # Sort network ranges to put "Native Range" first
            def sort_network_ranges(nr):
                # Native Range should come first (return 0), all others come after (return 1)
                range_name = nr.get('name', '')
                return 0 if range_name == 'Native Range' else 1
            
            sorted_network_ranges = sorted(network_ranges, key=sort_network_ranges)
            
            # Process all network ranges and identify which one is the native range
            # Native range is identified by the range name being "Native Range" 
            for idx, network_range in enumerate(sorted_network_ranges):
                # First row for this interface includes interface details
                is_first_range = (idx == 0)
                
                # Identify native range by checking if the range name is "Native Range"
                current_range_name = network_range.get('name', '')
                is_native_range = (current_range_name == 'Native Range')
                
                # Set lag_min_links to 1 only for LAN_LAG_MASTER interfaces (only on first row)
                lag_min_links_value = ''
                if is_first_range and 'LAN_LAG_MASTER' in interface_dest_type:
                    lag_min_links_value = '1'
                
                row = {
                    # LAN Interface details - first 3 columns reordered, lag_min_links 4th, is_native_range 5th, lan_interface_index 6th
                    'lan_interface_id': interface_id if is_first_range else '',
                    'lan_interface_name': interface_name if is_first_range else '',
                    'lan_interface_dest_type': interface_dest_type if is_first_range else '',
                    'lag_min_links': lag_min_links_value if is_first_range else '',  # Populated only for LAN_LAG_MASTER on first row
                    'is_native_range': 'TRUE' if is_native_range else '',  # TRUE only for native range, empty for others
                    'lan_interface_index': interface_index,  # Populated for every row
                    
                    # Network Range details (on all rows)
                    'network_range_id': network_range.get('id', ''),
                    'network_range_name': network_range.get('name', ''),
                    'subnet': network_range.get('subnet', ''),
                    'local_ip': network_range.get('local_ip', ''),
                    'gateway': network_range.get('gateway', ''),
                    'translated_subnet': network_range.get('translated_subnet', ''),
                    'vlan': network_range.get('vlan', ''),
                    'mdns_reflector': str(network_range.get('mdns_reflector', False)).upper(),
                    'range_type': network_range.get('range_type', ''),
                    'internet_only': network_range.get('internet_only', ''),
                    'dhcp_type': network_range.get('dhcp_settings', {}).get('dhcp_type', ''),
                    'dhcp_ip_range': network_range.get('dhcp_settings', {}).get('ip_range', ''),
                    'dhcp_relay_group_id': network_range.get('dhcp_settings', {}).get('relay_group_id', ''),
                    'dhcp_relay_group_name': network_range.get('dhcp_settings', {}).get('relay_group_name', ''),
                    'dhcp_microsegmentation': str(network_range.get('dhcp_settings', {}).get('dhcp_microsegmentation', False)).upper()
                }
                rows.append(row)
    
    # If still no rows, it means the site only has the default LAN interface (managed at parent level)
    # In this case, create an empty CSV file - no entries needed since default interface is handled in parent CSV
    # This is correct behavior: sites with only default LAN interfaces should have empty site-level CSV files
    
    # Always create file now (removed the early return)
    # if not rows:
    #     return None
    
    with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
        writer = csv.DictWriter(csvfile, fieldnames=headers)
        writer.writeheader()
        writer.writerows(rows)
    
    # Clean up CSV: remove empty lines and ensure proper line endings
    clean_csv_file(filepath, verbose=hasattr(args, 'verbose') and args.verbose)
    
    if hasattr(args, 'verbose') and args.verbose:
        print(f"Exported {len(rows)} network ranges for site '{site_name}' to {filepath}")
    
    return filepath


##########################################################################
########################### Helper functions #############################
##########################################################################

def populateSiteLocationData(args, site_data, cur_site):
    # Load site location data for timezone and state code lookups
    site_location_data = {}
    try:
        # Use package resource to load location data
        try:
            from importlib.resources import files
            location_data_str = files('catocli.parsers.custom.query_siteLocation').joinpath('query.siteLocation.json').read_text(encoding='utf-8')
            site_location_data = json.loads(location_data_str)
        except (ImportError, AttributeError):
            # Fallback for Python 3.7-3.8
            from importlib.resources import read_text
            location_data_str = read_text('catocli.parsers.custom.query_siteLocation', 'query.siteLocation.json', encoding='utf-8')
            site_location_data = json.loads(location_data_str)
        
        if hasattr(args, 'verbose') and args.verbose:
            print(f"Loaded {len(site_location_data)} location entries from package resource")
    except Exception as e:
        if hasattr(args, 'verbose') and args.verbose:
            print(f"Warning: Could not load site location data: {e}")

    address = site_data.get('infoSiteSnapshot', {}).get('address')
    city = site_data.get('infoSiteSnapshot', {}).get('cityName')                
    
    ## siteLocation attributes
    cur_site['site_location'] = {}
    cur_site['site_location']['stateName'] = site_data.get('infoSiteSnapshot', {}).get('countryStateName')
    cur_site['site_location']['countryCode'] = site_data.get('infoSiteSnapshot', {}).get('countryCode')
    cur_site['site_location']['countryName'] = site_data.get('infoSiteSnapshot', {}).get('countryName')
    cur_site['site_location']['address'] = address if address != "" else None
    cur_site['site_location']['city'] = city if city != "" else None

    # Look up timezone and state code from location data
    country_name = cur_site['site_location']['countryName']
    state_name = cur_site['site_location']['stateName']
    city = cur_site['site_location']['city']

    # Create lookup key based on available data
    location_data = {}
    
    if city:  # City is provided
        if state_name:
            lookup_key = f"{country_name}___{state_name}___{city}"
        else:
            lookup_key = f"{country_name}___{city}"
        
        # Debug output for lookup
        if hasattr(args, 'verbose') and args.verbose:
            print(f"Site {cur_site['name']}: Looking up '{lookup_key}'")
        
        # Look up location details
        location_data = site_location_data.get(lookup_key, {})
    
    elif state_name:  # No city, but state is provided - find any city in that state
        # Search for any entry matching country + state to get stateCode
        lookup_prefix = f"{country_name}___{state_name}___"
        
        if hasattr(args, 'verbose') and args.verbose:
            print(f"Site {cur_site['name']}: No city provided, searching for state '{state_name}' in '{country_name}'")
        
        # Find first matching entry for this state
        for key, data in site_location_data.items():
            if key.startswith(lookup_prefix):
                location_data = data
                if hasattr(args, 'verbose') and args.verbose:
                    print(f"  Found matching state entry: {key}")
                break
    
    # Now that location_data is defined, we can set stateCode
    if hasattr(args, 'verbose') and args.verbose:
        print("location_data:", json.dumps(location_data, indent=2))
    cur_site['site_location']['stateCode'] = location_data.get('stateCode', None)
    
    if hasattr(args, 'verbose') and args.verbose:
        if location_data:
            print(f"  Found location data: {location_data}")
        else:
            print(f"  No location data found for key: {lookup_key}")
            # Try to find similar keys for debugging
            similar_keys = [k for k in site_location_data.keys() if country_name in k and (not city or city in k)][:5]
            if similar_keys:
                print(f"  Similar keys found: {similar_keys}")


    # Get timezone - always use the 0 element in the timezones array
    timezones = location_data.get('timezone', [])
    timezone = timezones[0] if timezones else None
    
    # If timezone is still None, set a sensible default based on country
    if not timezone:
        # Default timezones for countries without city lookup
        country_default_timezones = {
            'FR': 'Europe/Paris',
            'US': 'America/New_York',
            'GB': 'Europe/London',
            'DE': 'Europe/Berlin',
            'JP': 'Asia/Tokyo',
            'AU': 'Australia/Sydney',
            'CA': 'America/Toronto',
            'IN': 'Asia/Kolkata',
            'CN': 'Asia/Shanghai',
            'BR': 'America/Sao_Paulo',
        }
        country_code = cur_site['site_location']['countryCode']
        timezone = country_default_timezones.get(country_code, 'UTC')
        
        if hasattr(args, 'verbose') and args.verbose:
            print(f"  No timezone found in location data, using default: {timezone}")
    
    cur_site['site_location']['timezone'] = timezone
    return cur_site

def getEntityLookup(args, configuration, account_id, entity_type, entity_ids=[], entity_input=[]):
    """
    Helper function to get entity lookup data for a specific entity type
    Implements pagination with increments of 50 records
    """
    #################################
    ## Get entity lookup for sites ##
    #################################
    all_items = []
    from_offset = 0
    limit = 50

    while True:
        entity_query = {
            "query": "query entityLookup ( $accountID:ID! $type:EntityType! $entityInput:EntityInput $entityIDs:[ID!] $from:Int $limit:Int ) { entityLookup ( accountID:$accountID type:$type parent:$entityInput entityIDs:$entityIDs from:$from limit:$limit ) { items { entity { id  name  type  } description helperFields } total  }  }",
            "variables": {
                "accountID": account_id,
                "type": entity_type,
                "from": from_offset,
                "limit": limit,
                "entityIDs": entity_ids,
                "entityInput": entity_input
            },
            "operationName": "entityLookup"
        }
        response = makeCall(args, configuration, entity_query)
        # Check for GraphQL errors in snapshot response
        if 'errors' in response:
            error_messages = [error.get('message', 'Unknown error') for error in response['errors']]
            raise Exception(f"Snapshot API returned errors: {', '.join(error_messages)}")

        if not response or 'data' not in response or 'entityLookup' not in response['data']:
            raise ValueError("Failed to retrieve snapshot data from API")

        items = response['data']['entityLookup']['items']
        if items is None:
            items = []

        # Append items from this page to all_items
        all_items.extend(items)

        items_count = len(items)
        if hasattr(args, 'verbose') and args.verbose:
            print(f"Retrieved {items_count} items for entity lookup - {entity_type} (offset: {from_offset})")

        # Stop if we received fewer items than the limit (last page)
        if items_count < limit:
            break

        # Increment offset for next page
        from_offset += limit

    if not all_items and hasattr(args, 'verbose') and args.verbose:
        print("No items found in entity lookup - "+ entity_type)

    return all_items

def getAccountSnapshot(args, configuration, account_id, site_ids=None):
    snapshot_query = {
        "query": "query accountSnapshot ( $siteIDs:[ID!] $accountID:ID ) { accountSnapshot ( accountID:$accountID ) { id sites ( siteIDs:$siteIDs ) { id protoId connectivityStatusSiteSnapshot: connectivityStatus haStatusSiteSnapshot: haStatus { readiness wanConnectivity keepalive socketVersion } operationalStatusSiteSnapshot: operationalStatus lastConnected connectedSince popName devices { id name identifier connected haRole interfaces { connected id name physicalPort naturalOrder popName previousPopID previousPopName tunnelConnectionReason tunnelUptime tunnelRemoteIP tunnelRemoteIPInfoInterfaceSnapshot: tunnelRemoteIPInfo { ip countryCode countryName city state provider latitude longitude } type infoInterfaceSnapshot: info { id name upstreamBandwidth downstreamBandwidth upstreamBandwidthMbpsPrecision downstreamBandwidthMbpsPrecision destType wanRole } cellularInterfaceInfoInterfaceSnapshot: cellularInterfaceInfo { networkType simSlotId modemStatus isModemConnected iccid imei operatorName isModemSuspended apn apnSelectionMethod signalStrength isRoamingAllowed simNumber disconnectionReason isSimSlot1Detected isSimSlot2Detected } } lastConnected lastDuration connectedSince lastPopID lastPopName recentConnections { duration interfaceName deviceName lastConnected popName remoteIP remoteIPInfoRecentConnection: remoteIPInfo { ip countryCode countryName city state provider latitude longitude } } type deviceUptime socketInfo { id serial isPrimary platformSocketInfo: platform version versionUpdateTime } interfacesLinkState { id up mediaIn linkSpeed duplex hasAddress hasInternet hasTunnel } osType osVersion version versionNumber releaseGroup mfaExpirationTime mfaCreationTime internalIP } infoSiteSnapshot: info { name type description countryCode region countryName countryStateName cityName address isHA connType creationTime interfaces { id name upstreamBandwidth downstreamBandwidth upstreamBandwidthMbpsPrecision downstreamBandwidthMbpsPrecision destType wanRoleInterfaceInfo: wanRole } sockets { id serial isPrimary platformSocketInfo: platform version versionUpdateTime } ipsec { isPrimary catoIP remoteIP ikeVersion } } hostCount altWanStatus } users { id connectivityStatusUserSnapshot: connectivityStatus operationalStatusUserSnapshot: operationalStatus name deviceName uptime lastConnected version versionNumber popID popName remoteIP remoteIPInfoUserSnapshot: remoteIPInfo { ip countryCode countryName city state provider latitude longitude } internalIP osType osVersion devices { id name identifier connected haRole interfaces { connected id name physicalPort naturalOrder popName previousPopID previousPopName tunnelConnectionReason tunnelUptime tunnelRemoteIP tunnelRemoteIPInfoInterfaceSnapshot: tunnelRemoteIPInfo { ip countryCode countryName city state provider latitude longitude } type infoInterfaceSnapshot: info { id name upstreamBandwidth downstreamBandwidth upstreamBandwidthMbpsPrecision downstreamBandwidthMbpsPrecision destType wanRole } cellularInterfaceInfoInterfaceSnapshot: cellularInterfaceInfo { networkType simSlotId modemStatus isModemConnected iccid imei operatorName isModemSuspended apn apnSelectionMethod signalStrength isRoamingAllowed simNumber disconnectionReason isSimSlot1Detected isSimSlot2Detected } } lastConnected lastDuration connectedSince lastPopID lastPopName recentConnections { duration interfaceName deviceName lastConnected popName remoteIP remoteIPInfoRecentConnection: remoteIPInfo { ip countryCode countryName city state provider latitude longitude } } type deviceUptime socketInfo { id serial isPrimary platformSocketInfo: platform version versionUpdateTime } interfacesLinkState { id up mediaIn linkSpeed duplex hasAddress hasInternet hasTunnel } osType osVersion version versionNumber releaseGroup mfaExpirationTime mfaCreationTime internalIP } connectedInOffice infoUserSnapshot: info { name status email creationTime phoneNumber origin authMethod } recentConnections { duration interfaceName deviceName lastConnected popName remoteIP remoteIPInfo { ip countryCode countryName city state provider latitude longitude } } } timestamp }  }",
        "variables": {
            "accountID": account_id,
            "siteIDs": site_ids
        },
        "operationName": "accountSnapshot"
    }
    response = makeCall(args, configuration, snapshot_query)

    # Check for GraphQL errors in snapshot response
    if 'errors' in response:
        error_messages = [error.get('message', 'Unknown error') for error in response['errors']]
        raise Exception(f"Snapshot API returned errors: {', '.join(error_messages)}")
    
    if not response or 'data' not in response or 'accountSnapshot' not in response['data']:
        raise ValueError("Failed to retrieve snapshot data from API")
    
    if not response or 'sites' not in response['data']['accountSnapshot'] or response['data']['accountSnapshot']['sites'] is None:
        # Instead of raising an exception, return an empty response structure
        response['data']['accountSnapshot']['sites'] = []

    return response