import fs from 'fs';
import csv from 'csv-parser';
import path from 'path';
import axios from 'axios';
import pLimit from 'p-limit';
import { Parser } from 'json2csv';

var merakiNetworks = [];


var config = JSON.parse(fs.readFileSync('./config/config.json'));

let lastPortFetchTime = 0;
const MIN_DELAY_MS = 150;       // ~6–7 calls/sec — safe under Meraki's 10/sec steady limit
const MAX_RETRIES = 5;
const BASE_BACKOFF_MS = 1000;   // 1s, 2s, 4s, 8s, 16s...

// Global cache for device info (serial → {model, name, type, ...})
const deviceInfoCache = new Map();


// Assume you have these functions from your previous code
// e.g., findMerakiNetworkId(storeNum) → returns network ID
// getMerakiClients(networkId) → returns array of clients with description, vlan, switchport, status, etc.

async function getMerakiDeviceInfo(serial) {
  if (!serial || serial === 'N/A') return null;

  if (deviceInfoCache.has(serial)) {
    return deviceInfoCache.get(serial);
  }

  const url = `https://api.meraki.com/api/v1/devices/${serial}`;

  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
          'Content-Type': 'application/json'
        }
      });

      if (response.status === 429) {
        let retryAfterSec = 2;
        const retryAfter = response.headers.get('retry-after');
        if (retryAfter) retryAfterSec = parseInt(retryAfter, 10) || 2;
        console.warn(`429 on device info for ${serial} - wait ${retryAfterSec}s`);
        await new Promise(r => setTimeout(r, retryAfterSec * 1000 + 300));
        continue;
      }

      if (!response.ok) {
        console.warn(`Device info fetch failed for ${serial}: HTTP ${response.status}`);
        return null;
      }

      const info = await response.json();
      deviceInfoCache.set(serial, info);
      return info;
    } catch (err) {
      console.warn(`Device info attempt ${attempt} error for ${serial}: ${err.message}`);
      if (attempt === 3) return null;
      await new Promise(r => setTimeout(r, 1000 * attempt));
    }
  }

  return null;
}

async function throttledGetMerakiSwitchPortConfig(serial, portId) {
  if (!serial || !portId) {
    throw new Error('Missing serial or portId for port config fetch');
  }

  const url = `https://api.meraki.com/api/v1/devices/${serial}/switch/ports/${portId}`;

  // Step 1: Enforce minimum delay between calls
  const now = Date.now();
  const timeSinceLast = now - lastPortFetchTime;
  if (timeSinceLast < MIN_DELAY_MS) {
    const waitMs = MIN_DELAY_MS - timeSinceLast;
    console.log(`Throttling port fetch for ${serial}/${portId} — waiting ${waitMs}ms`);
    await new Promise(resolve => setTimeout(resolve, waitMs));
  }

  // Update timestamp right before the request (more accurate for burst control)
  lastPortFetchTime = Date.now();

  // Step 2: Attempt the request with retries on 429 and transient errors
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
    try {
      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
          'Content-Type': 'application/json'
        }
      });

      // Handle 429 specifically
      if (response.status === 429) {
        let retryAfterSec = 2; // default fallback

        // Respect Retry-After header if present
        const retryAfterHeader = response.headers.get('retry-after');
        if (retryAfterHeader) {
          const parsed = parseInt(retryAfterHeader, 10);
          if (!isNaN(parsed) && parsed > 0) {
            retryAfterSec = parsed;
          }
        }

        console.warn(
          `429 Too Many Requests on ${serial}/${portId} ` +
          `(attempt ${attempt}/${MAX_RETRIES}) — waiting ${retryAfterSec}s`
        );

        await new Promise(resolve => setTimeout(resolve, retryAfterSec * 1000 + 200)); // +200ms jitter
        continue; // retry
      }

      if (!response.ok) {
        const errorText = await response.text().catch(() => 'Unknown error');
        throw new Error(`Meraki API failed (${response.status}): ${errorText}`);
      }

      const data = await response.json();
      console.log(`Successfully fetched port config for ${serial}/${portId}`);
      return data;

    } catch (err) {
      console.error(
        `Port fetch error for ${serial}/${portId} (attempt ${attempt}/${MAX_RETRIES}): ${err.message}`
      );

      if (attempt === MAX_RETRIES) {
        throw new Error(
          `Failed to fetch port ${portId} on switch ${serial} after ${MAX_RETRIES} attempts: ${err.message}`
        );
      }

      // Exponential backoff for non-429 errors too (e.g., 500s, network blips)
      const backoffMs = BASE_BACKOFF_MS * Math.pow(2, attempt - 1) + Math.random() * 200; // jitter
      console.log(`Backing off for ${Math.round(backoffMs)}ms before retry ${attempt + 1}`);
      await new Promise(resolve => setTimeout(resolve, backoffMs));
    }
  }

  // Should never reach here due to throw on last attempt
  throw new Error('Unexpected exit from retry loop');
}
// Helper: Fetch a single switch port configuration from Meraki API
async function getMerakiSwitchPortConfig(serial, portId) {
  if (!serial || !portId) {
    throw new Error('Missing serial or portId for port config fetch');
  }

  const url = `https://api.meraki.com/api/v1/devices/${serial}/switch/ports/${portId}`;

  try {
    const response = await fetch(url, {
      method: 'GET',
      headers: {
        'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
        'Content-Type': 'application/json'
      }
    });

    if (!response.ok) {
      const errorText = await response.text().catch(() => 'Unknown error');
      throw new Error(`Meraki API failed (${response.status}): ${errorText}`);
    }

    return await response.json();
  } catch (err) {
    throw new Error(`Failed to fetch port ${portId} on switch ${serial}: ${err.message}`);
  }
}

// Helper: Fetch all clients for a network (with basic pagination support)
async function getMerakiClients(networkId) {
  let allClients = [];
  let startingAfter = null;
  const perPage = 1000;

  do {
    let url = `https://api.meraki.com/api/v1/networks/${networkId}/clients?perPage=${perPage}`;
    if (startingAfter) {
      url += `&startingAfter=${encodeURIComponent(startingAfter)}`;
    }

    for (let attempt = 1; attempt <= 5; attempt++) {
      try {
        const response = await fetch(url, {
          method: 'GET',
          headers: {
            'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
            'Content-Type': 'application/json'
          }
        });

        if (response.status === 429) {
          let retryAfterSec = 2;
          const retryAfterHeader = response.headers.get('retry-after');
          if (retryAfterHeader) {
            const parsed = parseInt(retryAfterHeader, 10);
            if (!isNaN(parsed) && parsed > 0) retryAfterSec = parsed;
          }
          console.warn(`429 on clients page for ${networkId} (attempt ${attempt}) - waiting ${retryAfterSec}s`);
          await new Promise(r => setTimeout(r, retryAfterSec * 1000 + 300)); // jitter
          continue;
        }

        if (!response.ok) {
          throw new Error(`Clients fetch failed: ${response.status}`);
        }

        const clients = await response.json();
        allClients = allClients.concat(clients);
        startingAfter = clients.length === perPage ? clients[clients.length - 1].id : null;
        break; // success → next page

      } catch (err) {
        if (attempt === 5) throw err;
        const backoff = 1000 * Math.pow(2, attempt - 1) + Math.random() * 500;
        console.log(`Retry backoff ${Math.round(backoff)}ms for ${networkId}`);
        await new Promise(r => setTimeout(r, backoff));
      }
    }
  } while (startingAfter);

  return allClients;
}

async function getAllMerakiSwitchPorts(serial) {
  if (!serial || serial === 'N/A') return [];

  // Check if it's actually a switch
  const device = await getMerakiDeviceInfo(serial);
  if (!device || !device.model?.startsWith('MS')) {
    const model = device?.model || 'unknown';
    console.log(`Skipping ports fetch for non-switch device ${serial} (model: ${model})`);
    return [];
  }

  console.log(`Fetching ports for confirmed switch ${serial} (model: ${device.model})`);

  const url = `https://api.meraki.com/api/v1/devices/${serial}/switch/ports`;

  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
          'Content-Type': 'application/json'
        }
      });

      if (response.status === 429) {
        let retryAfterSec = 2;
        const retryAfter = response.headers.get('retry-after');
        if (retryAfter) retryAfterSec = parseInt(retryAfter, 10) || 2;
        console.warn(`429 on ports fetch for ${serial} - wait ${retryAfterSec}s`);
        await new Promise(r => setTimeout(r, retryAfterSec * 1000 + 300));
        continue;
      }

      if (!response.ok) {
        const errText = await response.text().catch(() => 'Unknown');
        throw new Error(`HTTP ${response.status}: ${errText}`);
      }

      const ports = await response.json();
      console.log(`Fetched ${ports.length} ports for switch ${serial}`);
      return ports;
    } catch (err) {
      console.warn(`Ports fetch attempt ${attempt} failed for ${serial}: ${err.message}`);
      if (attempt === 3) return [];
      await new Promise(r => setTimeout(r, 1000 * attempt));
    }
  }

  return [];
}

// Main function – now outputs CSV directly
async function processCSVAndEnrichMeraki(csvFilePath, outputCsvPath = 'enriched-meraki-output.csv') {
  const results = [];

  // 1. Parse input CSV
  await new Promise((resolve, reject) => {
    fs.createReadStream(csvFilePath)
      .pipe(csv())
      .on('data', row => results.push(row))
      .on('end', () => {
        console.log(`Input CSV parsed - ${results.length} rows`);
        resolve();
      })
      .on('error', err => reject(err));
  });

  // 2. Cache networks if not already done
  if (merakiNetworks.length === 0) {
    await cacheMerakiNetworks();
  }

  // 3. Build unique store → network map
  const storeToNetwork = new Map();
  const limit = pLimit(4);

  const networkPromises = results.map(row => {
    const friendlyName = row.device_friendly_name?.trim() || '';
    if (!friendlyName) return Promise.resolve();
    const storeNum = friendlyName.match(/(\d{6})/)?.[0];
    if (!storeNum || storeToNetwork.has(storeNum)) return Promise.resolve();

    return limit(() =>
      findMerakiNetworkId(storeNum)
        .then(netId => { if (netId) storeToNetwork.set(storeNum, netId); })
        .catch(() => { })
    );
  });

  await Promise.all(networkPromises);
  console.log(`Found ${storeToNetwork.size} unique networks`);

  // 4. Pre-fetch clients per network
  const networkClientCache = new Map();
  const clientPromises = [...storeToNetwork.entries()].map(([storeNum, netId]) =>
    limit(async () => {
      try {
        const clients = await getMerakiClients(netId);
        networkClientCache.set(netId, clients);
        console.log(`Clients fetched: ${clients.length} for store ${storeNum}`);
      } catch (err) {
        console.error(`Clients fetch failed for store ${storeNum}: ${err.message}`);
        networkClientCache.set(netId, []);
      }
    })
  );

  await Promise.all(clientPromises);

  // 5. Enrich rows – collect unique serials first
  const enrichedRows = results.map(row => ({ ...row, meraki: null, error: null }));
  const serialToPorts = new Map();
  const uniqueSerials = new Set();

  // Phase A: Match clients & collect serials
  for (let i = 0; i < results.length; i++) {
    const row = results[i];
    const friendlyName = row.device_friendly_name?.trim() || '';
    if (!friendlyName) {
      enrichedRows[i].error = 'Missing friendly name';
      continue;
    }

    if (match.recentDeviceConnection !== 'Wired' || !match.switchport) {
      enrichedRows[i].error = 'Wireless or non-switch connected client';
      continue;
    }

    const storeNum = friendlyName.match(/(\d{6})/)?.[0];
    if (!storeNum) continue;

    const netId = storeToNetwork.get(storeNum);
    if (!netId) continue;

    const clients = networkClientCache.get(netId) || [];
    const match = clients.find(c =>
      c.description?.toUpperCase().trim() === friendlyName.toUpperCase().trim()
    );

    if (!match) {
      enrichedRows[i].error = 'No matching Meraki client';
      continue;
    }

    const merakiInfo = {
      deviceName: match.recentDeviceName || 'N/A',
      deviceSerial: match.recentDeviceSerial || 'N/A',
      switchport: match.switchport || 'N/A',
      vlan: match.vlan || 'N/A',
      status: match.status || 'Unknown',
      connection: match.recentDeviceConnection || 'N/A',
      lastSeen: match.lastSeen || 'N/A',
      ip: match.ip || 'N/A',
      mac: match.mac || 'N/A'
    };

    enrichedRows[i].meraki = merakiInfo;

    if (merakiInfo.deviceSerial !== 'N/A') {
      uniqueSerials.add(merakiInfo.deviceSerial);
    }
  }

  // Phase B: Pre-fetch all ports for unique switches
  if (uniqueSerials.size > 0) {
    console.log(`Fetching ports for ${uniqueSerials.size} unique switches...`);
    const portPromises = [...uniqueSerials].map(serial =>
      limit(async () => {
        const ports = await getAllMerakiSwitchPorts(serial);
        serialToPorts.set(serial, ports);
      })
    );
    await Promise.all(portPromises);
  }

  // Phase C: Final enrichment from cache
  for (const row of enrichedRows) {
    const meraki = row.meraki;
    if (!meraki || meraki.deviceSerial === 'N/A' || meraki.switchport === 'N/A') {
      if (meraki) {
        Object.assign(meraki, {
          portType: 'N/A',
          accessPolicyType: 'N/A',
          stickyMacEnabled: false,
          stickyMacList: [],
          stickyMacAllowListLimit: 0,
          stickyMacCount: 0,
          portEnabled: 'N/A'
        });
      }
      continue;
    }

    const ports = serialToPorts.get(meraki.deviceSerial) || [];
    const portConfig = ports.find(p =>
      String(p.port || p.portId || p.number) === String(meraki.switchport)
    );

    if (portConfig) {
      meraki.portType = portConfig.type || 'N/A';
      meraki.accessPolicyType = portConfig.accessPolicyType || 'N/A';
      meraki.stickyMacEnabled = portConfig.accessPolicyType === 'Sticky MAC allow list';
      meraki.stickyMacList = portConfig.stickyMacAllowList || [];
      meraki.stickyMacAllowListLimit = portConfig.stickyMacAllowListLimit || 0;
      meraki.stickyMacCount = meraki.stickyMacList.length;
      meraki.portEnabled = portConfig.enabled ? 'Enabled' : 'Disabled';
    } else {
      meraki.portType = 'Not Found';
      meraki.portEnabled = 'Not Found';
    }
  }

  // 6. Convert enriched data → CSV
  const flattened = enrichedRows.map(item => {
    const m = item.meraki || {};
    const stickyList = Array.isArray(m.stickyMacList) ? m.stickyMacList.join(', ') : '';

    return {
      device_id: item.device_id || '',
      device_friendly_name: item.device_friendly_name || '',
      device_enrollment_user_name: item.device_enrollment_user_name || '',
      _device_platform: item._device_platform || '',
      device_os_version: item.device_os_version || '',
      device_last_seen_utc: item.device_last_seen_utc || '',
      device_enrollment_status: item.device_enrollment_status || '',
      device_model_name: item.device_model_name || '',
      _device_mac_address: item._device_mac_address || '',
      device_mac_address: item.device_mac_address || '',

      meraki_deviceName: m.deviceName || '',
      meraki_deviceSerial: m.deviceSerial || '',
      meraki_switchport: m.switchport || '',
      meraki_vlan: m.vlan || '',
      meraki_status: m.status || '',
      meraki_connection: m.connection || '',
      meraki_lastSeen: m.lastSeen || '',
      meraki_ip: m.ip || '',
      meraki_mac: m.mac || '',

      meraki_portType: m.portType || 'N/A',
      meraki_accessPolicyType: m.accessPolicyType || 'N/A',
      meraki_portEnabled: m.portEnabled || 'N/A',
      meraki_stickyMacEnabled: m.stickyMacEnabled === true ? 'true' : 'false',
      meraki_stickyMacList: stickyList,
      meraki_stickyMacAllowListLimit: m.stickyMacAllowListLimit ?? '',
      meraki_stickyMacCount: m.stickyMacCount ?? '',
      error: item.error || ''
    };
  });

  const fields = [
    'device_id', 'device_friendly_name', 'device_enrollment_user_name', '_device_platform',
    'device_os_version', 'device_last_seen_utc', 'device_enrollment_status', 'device_model_name',
    '_device_mac_address', 'device_mac_address',
    'meraki_deviceName', 'meraki_deviceSerial', 'meraki_switchport', 'meraki_vlan',
    'meraki_status', 'meraki_connection', 'meraki_lastSeen', 'meraki_ip', 'meraki_mac',
    'meraki_portType', 'meraki_accessPolicyType', 'meraki_portEnabled',
    'meraki_stickyMacEnabled', 'meraki_stickyMacList', 'meraki_stickyMacAllowListLimit',
    'meraki_stickyMacCount', 'error'
  ];

  const parser = new Parser({ fields });
  const csvContent = parser.parse(flattened);
  fs.writeFileSync('enriched-meraki-intermediate.json', JSON.stringify(enrichedRows, null, 2));
  fs.writeFileSync(outputCsvPath, csvContent);
  console.log(`Enrichment complete. Output CSV saved to: ${outputCsvPath}`);
  console.log(`Processed ${enrichedRows.length} rows`);

  return enrichedRows; // optional – for further chaining if needed
}

async function findMerakiNetworkId(storeNum) {
  var startTime = new Date().getTime();
  var networkSearchTerm = Number(storeNum).toString().padStart(5, "0");
  try {
    // Filter networks by partial name match (case-insensitive)
    const matchingNetworks = merakiNetworks.filter(net =>
      (net.name || '').toLowerCase().includes(networkSearchTerm.toLowerCase())
    );

    if (matchingNetworks.length === 0) {
      return null;
    }

    if (matchingNetworks.length > 1) {
      console.log(`Multiple networks match '${networkSearchTerm}':`);
      matchingNetworks.forEach(net => console.log(`- ${net.name} (ID: ${net.id})`));
      console.log('Using the first match...');
    }


    return matchingNetworks[0].id;
  } catch (error) {
    console.error(`Error finding Meraki Network: ${error}`);
    return null;
  }
}

async function cacheMerakiNetworks() {

  try {
    let allNetworks = [];
    let nextUrl = `https://api.meraki.com/api/v1/organizations/${config.auth.meraki.orgId}/networks?perPage=1000`;

    while (nextUrl) {
      const response = await axios.get(nextUrl, {
        headers: {
          'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
          'Content-Type': 'application/json'
        }
      });

      const pageNetworks = response.data;
      allNetworks = allNetworks.concat(pageNetworks);

      //logger(`cacheMerakiNetworks()`, `Fetched ${pageNetworks.length} networks (total so far: ${allNetworks.length})`);

      // Log raw header
      //logger(`cacheMerakiNetworks()`, `Link header raw: ${response.headers.link || '(none)'}`);

      // Robust next URL extraction
      const linkHeader = response.headers.link;
      let foundNext = null;

      if (linkHeader) {
        //logger(`cacheMerakiNetworks()`, `Full Link header (raw): ${linkHeader}`);

        const parts = linkHeader.split(',');
        //logger(`cacheMerakiNetworks()`, `Split into ${parts.length} parts`);

        for (const part of parts) {
          const trimmed = part.trim();
          //logger(`cacheMerakiNetworks()`, `Examining part: "${trimmed}"`);

          // Forgiving checks: lower case, no quotes required, partial match
          const lowerTrimmed = trimmed.toLowerCase();
          if (lowerTrimmed.includes('rel=next') || lowerTrimmed.includes('rel="next"') || lowerTrimmed.includes("rel='next'")) {
            //logger(`cacheMerakiNetworks()`, `→ Detected rel=next in: "${trimmed}"`);

            const urlMatch = trimmed.match(/<([^>]+)>/);
            if (urlMatch && urlMatch[1]) {
              foundNext = urlMatch[1].trim();  // extra trim just in case
              //logger(`cacheMerakiNetworks()`, `→ Extracted next URL: ${foundNext}`);
              break;
            } else {
              //logger(`cacheMerakiNetworks()`, `→ URL match failed on that part`);
            }
          }
        }
      }

      nextUrl = foundNext;

      if (!nextUrl) {
        //logger(`cacheMerakiNetworks()`, `No next page detected – ending loop`);
      } else {
        //logger(`cacheMerakiNetworks()`, `Advancing to next URL: ${nextUrl}`);
      }
    }

    merakiNetworks = allNetworks;
    logger(`cacheMerakiNetworks()`, `Cached ${merakiNetworks.length} networks. (${new Date().getTime() - startTime}ms)`);

  } catch (error) {
    console.error('Meraki Networks API error:', error.message);
    if (error.response) {
      console.error('Status:', error.response.status);
      console.error('Data:', error.response.data);
    }
  }
}

async function rebootWorkspaceOneDevice(deviceId) {
  const baseUrl = `https://${awHost}/api`;  // e.g., as123.awmdm.com
  const headers = {
    'Authorization': `Basic ${Buffer.from('your_username:your_password').toString('base64')}`, // or use API key method
    'aw-tenant-code': tenantCode,
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  };

  try {
    const response = await axios.post(
      `${baseUrl}/mdm/devices/${deviceId}/commands`,
      { Command: 'RebootDevice' },  // or 'RestartDevice' for iOS/tvOS
      { headers }
    );

    console.log('Reboot command sent successfully:', response.data);
    return response.data;
  } catch (error) {
    console.error('Error sending reboot:');
    console.error(error.response?.data || error.message);
    throw error;
  }
}
/*
async function updateSwitchPort(networkId, serial, portId, settings) {
  const apiKey = 'YOUR_MERAKI_API_KEY_HERE';
  const baseUrl = 'https://api.meraki.com/api/v1';

  try {
    const response = await axios.put(
      `${baseUrl}/networks/${networkId}/devices/${serial}/switch/ports/${portId}`,
      settings,
      {
        headers: {
          'X-Cisco-Meraki-API-Key': apiKey,
          'Content-Type': 'application/json'
        }
      }
    );

    console.log('Success:', response.data);
    return response.data;
  } catch (error) {
    console.error('Error updating port:');
    console.error(error.response?.data || error.message);
    throw error;
  }
}

// Usage examples
updateSwitchPort('L_123456789012345678', 'Q3LU-ABCDE-12345', '8', false);   // Disable port 8
// updateSwitchPort('L_123456789012345678', 'Q3LU-ABCDE-12345', '8', true);   // Enable port 8*/
function logger(functionName, message) {
  var d = new Date();
  var year = d.getFullYear();
  var month = (d.getMonth() + 1).toString().padStart(2, "0");
  var day = d.getDate().toString().padStart(2, "0");
  let logFile = path.join(`./logs/${year}${month}${day}.log`);
  console.log(d.toLocaleString() + " " + functionName + ": " + message);
  fs.appendFileSync(logFile, d.toLocaleString() + " " + functionName + ": " + message + "\n");
}

async function sendMdmQuery(deviceId, apiKey, tenantCode, awHost) {
  const baseUrl = `https://${awHost}/api`;  // e.g., as1991.awmdm.com

  const headers = {
    'Authorization': `Basic ${Buffer.from('your_username:your_password').toString('base64')}`, // or API key method
    'aw-tenant-code': tenantCode,
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  };

  try {
    const response = await axios.post(
      `${baseUrl}/mdm/devices/${deviceId}/commands`,
      { Command: 'QueryDevice' },
      { headers }
    );

    console.log('Query command sent successfully:', response.data);
    return response.data;
  } catch (error) {
    console.error('Error sending MDM Query:');
    if (error.response) {
      console.error('Status:', error.response.status);
      console.error('Response:', error.response.data);
    } else {
      console.error(error.message);
    }
    throw error;
  }
}

// Usage example
const csvFile = 'appleTV.csv';
const inputCsv = 'appleTV.csv';
await processCSVAndEnrichMeraki(inputCsv, 'enriched-meraki-output.csv')
  .catch(err => console.error('Fatal error:', err));