// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
//     import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
//     import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.

// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
import { hooks as colocatedHooks } from "phoenix-colocated/towerops"
import topbar from "../vendor/topbar"
import Chart from "../vendor/chart"
import type { ChartData, ChartDataset, ChartDataPoint } from "../vendor/chart"
import cytoscape from "../vendor/cytoscape"
import type { SensorChartHook } from "./types/liveview"
import { DeviceListReorder } from "./device_list_reorder"
import { initCookieConsent } from "./cookie_consent"

// Helper function to convert range string to milliseconds
function getTimeRangeMs(range: string): number {
  switch (range) {
    case '1h': return 1 * 60 * 60 * 1000
    case '6h': return 6 * 60 * 60 * 1000
    case '12h': return 12 * 60 * 60 * 1000
    case '24h': return 24 * 60 * 60 * 1000
    case '7d': return 7 * 24 * 60 * 60 * 1000
    case '30d': return 30 * 24 * 60 * 60 * 1000
    default: return 24 * 60 * 60 * 1000  // Default to 24 hours
  }
}

// Generic Sensor Chart Hook (for CPU, Memory, Storage, etc.)
const SensorChart: SensorChartHook = {
  mounted() {
    this.isLiveMode = this.el?.dataset.range === 'live'
    this.createChart()

    // Listen for live updates
    this.handleEvent?.("live_data_update", (data: any) => {
      this.handleLiveDataUpdate(data)
    })
  },

  updated() {
    const newRange = this.el?.dataset.range
    const wasLiveMode = this.isLiveMode
    this.isLiveMode = newRange === 'live'

    // Mode switch - recreate chart
    if (wasLiveMode !== this.isLiveMode) {
      if (this.chart) {
        this.chart.destroy()
      }
      this.createChart()
    } else if (!this.isLiveMode) {
      // Historical mode - recreate chart (existing behavior)
      if (this.chart) {
        this.chart.destroy()
      }
      this.createChart()
    }
    // Live mode - updates handled by event, don't recreate
  },

  destroyed() {
    if (this.chart) {
      this.chart.destroy()
      this.chart = null
    }
  },

  createChart() {
    if (!this.el) return

    const dataAttr = this.el.dataset.chart
    if (!dataAttr) return

    const data: ChartData = JSON.parse(dataAttr)
    const canvas = this.el.querySelector('canvas')
    if (!canvas) return

    const ctx = canvas.getContext('2d')
    if (!ctx) return

    const unit = this.el.dataset.unit ?? '%'
    const autoScale = this.el.dataset.autoScale === 'true' || this.el.dataset.autoScale === 'true'
    const showZeroLine = this.el.dataset.showZeroLine === 'true' || this.el.dataset.showZeroLine === 'true'
    const range = this.el.dataset.range || '24h'
    const isTrafficChart = unit === 'bps'

    // Generate colors for each dataset
    const colors = [
      'rgb(59, 130, 246)',   // blue
      'rgb(239, 68, 68)',    // red
      'rgb(34, 197, 94)',    // green
      'rgb(234, 179, 8)',    // yellow
      'rgb(168, 85, 247)',   // purple
      'rgb(236, 72, 153)',   // pink
      'rgb(14, 165, 233)',   // sky
      'rgb(249, 115, 22)',   // orange
    ]

    const datasets: ChartDataset[] = data.datasets.map((dataset, index) => {
      const color = colors[index % colors.length]
      const baseConfig: ChartDataset = {
        ...dataset,
        borderColor: color,
        backgroundColor: color.replace('rgb', 'rgba').replace(')', ', 0.2)'),
        borderWidth: 2,
        tension: 0.4,
        pointRadius: 0,
        pointHoverRadius: 4,
      }

      // Add fill for traffic charts to create area effect
      if (isTrafficChart) {
        baseConfig.fill = 'origin'  // Fill to the zero line
      } else {
        baseConfig.fill = false  // No fill for CPU/memory/storage charts
      }

      return baseConfig
    })

    // Format bits per second into human-readable units
    const formatBps = (bps: number): string => {
      // Use absolute value for display (inbound is negative, but we show it as positive)
      const absBps = Math.abs(bps)
      if (absBps >= 1_000_000_000) {
        return (absBps / 1_000_000_000).toFixed(2) + ' Gbps'
      } else if (absBps >= 1_000_000) {
        return (absBps / 1_000_000).toFixed(2) + ' Mbps'
      } else if (absBps >= 1_000) {
        return (absBps / 1_000).toFixed(2) + ' Kbps'
      } else {
        return absBps.toFixed(2) + ' bps'
      }
    }

    // For traffic graphs, calculate symmetric scale to keep zero in the middle
    let yAxisConfig: any = {}
    if (autoScale) {
      if (showZeroLine && unit === 'bps') {
        // Find max absolute value to make symmetric scale
        let maxAbsValue = 0
        datasets.forEach(dataset => {
          if (dataset.data && dataset.data.length > 0) {
            dataset.data.forEach((point: ChartDataPoint) => {
              if (point && typeof point.y === 'number') {
                maxAbsValue = Math.max(maxAbsValue, Math.abs(point.y))
              }
            })
          }
        })
        // Add 10% padding, ensure minimum of 1000 (1 Kbps)
        maxAbsValue = Math.max(maxAbsValue * 1.1, 1000)

        yAxisConfig = {
          min: -maxAbsValue,
          max: maxAbsValue,
          ticks: {
            callback: function(value: number) {
              return formatBps(value)
            }
          },
          grid: {
            color: function(context: any) {
              // Make the zero line more prominent
              if (context.tick.value === 0) {
                return 'rgba(0, 0, 0, 0.3)'
              }
              return 'rgba(0, 0, 0, 0.05)'
            },
            lineWidth: function(context: any) {
              // Make the zero line thicker
              if (context.tick.value === 0) {
                return 2
              }
              return 1
            }
          }
        }
      } else {
        yAxisConfig = {
          beginAtZero: showZeroLine,
          ticks: {
            callback: function(value: number) {
              if (unit === 'bps') {
                return formatBps(value)
              }
              return value + unit
            }
          },
          grid: {
            color: function(context: any) {
              // Make the zero line more prominent
              if (showZeroLine && context.tick.value === 0) {
                return 'rgba(0, 0, 0, 0.3)'
              }
              return 'rgba(0, 0, 0, 0.05)'
            },
            lineWidth: function(context: any) {
              // Make the zero line thicker
              if (showZeroLine && context.tick.value === 0) {
                return 2
              }
              return 1
            }
          }
        }
      }
    } else {
      yAxisConfig = {
        min: 0,
        max: 100,
        ticks: {
          callback: function(value: number) {
            if (unit === 'bps') {
              return formatBps(value)
            }
            return value + unit
          }
        },
        grid: {
          color: 'rgba(0, 0, 0, 0.05)'
        }
      }
    }

    // Calculate time range based on selected range
    const now = Date.now()
    let xAxisConfig: any

    if (this.isLiveMode) {
      // Live mode - 5 minute window
      xAxisConfig = {
        type: 'linear',
        min: now - (5 * 60 * 1000),
        max: now,
        ticks: {
          callback: function(value: number) {
            const date = new Date(value)
            return date.toLocaleTimeString('en-US', {
              hour: '2-digit',
              minute: '2-digit',
              second: '2-digit',
              hour12: false
            })
          },
          maxTicksLimit: 10
        },
        grid: {
          display: false
        }
      }
    } else {
      // Historical mode - existing configuration
      const timeRangeMs = getTimeRangeMs(range)
      const rangeStart = now - timeRangeMs

      xAxisConfig = {
        type: 'linear',
        min: rangeStart,
        max: now,
        ticks: {
          callback: function(value: number) {
            const date = new Date(value)
            // For ranges > 24h, show date + time; otherwise just time
            if (timeRangeMs > 24 * 60 * 60 * 1000) {
              return date.toLocaleString('en-US', {
                month: 'short',
                day: 'numeric',
                hour: '2-digit',
                minute: '2-digit',
                hour12: false
              })
            } else {
              return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })
            }
          },
          maxTicksLimit: 12
        },
        grid: {
          display: false
        }
      }
    }

    this.chart = new Chart(ctx, {
      type: 'line',
      data: { datasets },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        animation: false,
        interaction: {
          mode: 'index',
          intersect: false,
        },
        plugins: {
          legend: {
            position: 'bottom',
            labels: {
              usePointStyle: true,
              padding: 15,
            }
          },
          tooltip: {
            callbacks: {
              title: function(context: any) {
                const timestamp = context[0].parsed.x
                const date = new Date(timestamp)
                return date.toLocaleString('en-US', {
                  month: 'short',
                  day: 'numeric',
                  hour: '2-digit',
                  minute: '2-digit',
                  hour12: false
                })
              },
              label: function(context: any) {
                if (unit === 'bps') {
                  return context.dataset.label + ': ' + formatBps(context.parsed.y)
                }
                return context.dataset.label + ': ' + context.parsed.y.toFixed(1) + unit
              }
            }
          }
        },
        scales: {
          x: xAxisConfig,
          y: yAxisConfig
        }
      }
    })
  },

  handleLiveDataUpdate(event: any) {
    if (!this.chart || !this.isLiveMode) return

    const { points } = event
    const now = Date.now()

    // Update each dataset with new point
    points.forEach((point: any) => {
      const dataset = this.chart!.data.datasets.find(
        (ds: any) => ds.label === point.label
      )

      if (dataset && dataset.data) {
        // Add new point
        (dataset.data as any[]).push({
          x: point.timestamp,
          y: point.value
        })

        // Remove points older than 5 minutes
        const cutoff = now - (5 * 60 * 1000)
        dataset.data = (dataset.data as any[]).filter(
          (p: any) => p.x >= cutoff
        )
      } else if (!dataset) {
        // New sensor - add dataset
        const colors = [
          'rgb(59, 130, 246)',   // blue
          'rgb(239, 68, 68)',    // red
          'rgb(34, 197, 94)',    // green
          'rgb(234, 179, 8)',    // yellow
          'rgb(168, 85, 247)',   // purple
          'rgb(236, 72, 153)',   // pink
          'rgb(14, 165, 233)',   // sky
          'rgb(249, 115, 22)',   // orange
        ]
        const index = this.chart!.data.datasets.length
        const color = colors[index % colors.length]

        this.chart!.data.datasets.push({
          label: point.label,
          data: [{x: point.timestamp, y: point.value}],
          borderColor: color,
          backgroundColor: color.replace('rgb', 'rgba').replace(')', ', 0.2)'),
          borderWidth: 2,
          tension: 0.4,
          pointRadius: 0,
          pointHoverRadius: 4,
          fill: false
        } as any)
      }
    })

    // Update x-axis range to keep 5-minute window
    if (this.chart.options.scales?.x) {
      this.chart.options.scales.x.min = now - (5 * 60 * 1000)
      this.chart.options.scales.x.max = now
    }

    // Update chart without animation
    this.chart.update('none')
  }
}

// LiveView hook for copying to clipboard
const CopyToClipboard = {
  handleClick: null as ((e: Event) => void) | null,

  mounted(this: any) {
    this.handleClick = (e: Event) => {
      e.preventDefault()
      const targetId = this.el.dataset.target
      if (!targetId) {
        console.error("No data-target attribute found on copy button")
        return
      }

      const targetEl = document.querySelector(targetId) as HTMLInputElement | HTMLTextAreaElement | HTMLElement
      if (!targetEl) {
        console.error(`Target element not found: ${targetId}`)
        return
      }

      let text = ""
      if (targetEl.tagName === "INPUT" || targetEl.tagName === "TEXTAREA") {
        text = (targetEl as HTMLInputElement | HTMLTextAreaElement).value
      } else {
        text = targetEl.innerText || targetEl.textContent || ""
      }

      // Trim any whitespace from the text
      text = text.trim()

      if (text) {
        navigator.clipboard.writeText(text).then(() => {
          console.log('Copied to clipboard successfully')
          // Show visual feedback
          const originalHTML = this.el.innerHTML
          const originalClasses = this.el.className

          // Update button to show success state
          this.el.innerHTML = '<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg><span>Copied!</span>'
          this.el.className = originalClasses.replace('bg-blue-600', 'bg-green-600').replace('hover:bg-blue-700', 'hover:bg-green-700').replace('bg-blue-500', 'bg-green-500').replace('hover:bg-blue-600', 'hover:bg-green-600')

          setTimeout(() => {
            this.el.innerHTML = originalHTML
            this.el.className = originalClasses
          }, 2000)
        }).catch(err => {
          console.error('Failed to copy:', err)
          // Show error state
          const originalHTML = this.el.innerHTML
          const originalClasses = this.el.className

          this.el.innerHTML = '<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg><span>Failed</span>'
          this.el.className = originalClasses.replace('bg-blue-600', 'bg-red-600').replace('hover:bg-blue-700', 'hover:bg-red-700').replace('bg-blue-500', 'bg-red-500').replace('hover:bg-blue-600', 'hover:bg-red-600')

          setTimeout(() => {
            this.el.innerHTML = originalHTML
            this.el.className = originalClasses
          }, 2000)
        })
      } else {
        console.error('No text found to copy')
      }
    }

    this.el.addEventListener("click", this.handleClick)
  },

  destroyed(this: any) {
    if (this.handleClick) {
      this.el.removeEventListener("click", this.handleClick)
      this.handleClick = null
    }
  }
}

// LiveView hook for scrolling to top
const ScrollToTop = {
  mounted() {
    this.handleEvent("scroll_to_top", () => {
      window.scrollTo({ top: 0, behavior: "smooth" })
    })
  }
}

// LiveView hook for auto-dismissing flash messages
const AutoDismissFlash = {
  timeout: null as ReturnType<typeof setTimeout> | null,
  mounted(this: any) {
    const dismissAfter = parseInt(this.el.dataset.dismissAfter || "30000", 10)
    this.timeout = setTimeout(() => {
      this.el.click()
    }, dismissAfter)
  },
  destroyed(this: any) {
    if (this.timeout) {
      clearTimeout(this.timeout)
      this.timeout = null
    }
  }
}

// LiveView hook for dismissible beta banner
const BetaBannerDismiss = {
  handleDismiss: null as ((e: Event) => void) | null,

  mounted(this: any) {
    this.applyBannerState()
  },

  updated(this: any) {
    this.applyBannerState()
  },

  applyBannerState(this: any) {
    const isHelpPage = this.el.dataset.alwaysShow === "true"

    if (isHelpPage) {
      // On help page: hide dismiss button only
      const dismissBtn = this.el.querySelector('[data-dismiss]')
      if (dismissBtn) {
        (dismissBtn as HTMLElement).style.display = 'none'
      }
    } else {
      // On other pages: check if dismissed and set up dismiss handler
      const isDismissed = localStorage.getItem('betaBannerDismissed') === 'true'
      if (isDismissed) {
        this.el.style.display = 'none'
      }

      // Only set up event listener once (in mounted)
      if (!this.handleDismiss) {
        const dismissBtn = this.el.querySelector('[data-dismiss]')
        if (dismissBtn) {
          this.handleDismiss = (e: Event) => {
            e.preventDefault()
            localStorage.setItem('betaBannerDismissed', 'true')
            this.el.style.display = 'none'
          }
          dismissBtn.addEventListener('click', this.handleDismiss)
        }
      }
    }
  },

  destroyed(this: any) {
    const dismissBtn = this.el.querySelector('[data-dismiss]')
    if (dismissBtn && this.handleDismiss) {
      dismissBtn.removeEventListener('click', this.handleDismiss)
      this.handleDismiss = null
    }
  }
}

// Network Map hook for Cytoscape.js topology visualization
const NetworkMap = {
  cy: null as any,
  mounted(this: any) {
    const topologyData = JSON.parse(this.el.dataset.topology || '{}')
    this.initializeCytoscape(topologyData)

    // Listen for topology updates from LiveView
    this.handleEvent("update_topology", (payload: any) => {
      if (this.cy) {
        this.updateTopology(payload.topology)
      }
    })
  },

  updated(this: any) {
    const topologyData = JSON.parse(this.el.dataset.topology || '{}')
    this.updateTopology(topologyData)
  },

  destroyed(this: any) {
    if (this.cy) {
      this.cy.destroy()
      this.cy = null
    }
  },

  initializeCytoscape(this: any, topology: any) {
    const isDark = document.documentElement.getAttribute('data-theme') === 'dark'

    // Role-based node colors (keyed by the `type` field from backend)
    const roleColors: Record<string, string> = {
      router: '#3B82F6',     // blue
      switch: '#10B981',     // green
      wireless: '#8B5CF6',   // purple
      firewall: '#EF4444',   // red
      server: '#F59E0B',     // amber
      unknown: '#9CA3AF'     // light gray
    }

    // Role-based node shapes
    const roleShapes: Record<string, string> = {
      router: 'round-rectangle',
      switch: 'diamond',
      wireless: 'triangle',
      firewall: 'hexagon',
      server: 'rectangle',
      unknown: 'ellipse'
    }

    // Confidence-based edge styling helper
    const getEdgeStyle = (confidence: number) => {
      if (confidence > 0.9) {
        return { color: '#10B981', style: 'solid', dashPattern: [] }
      } else if (confidence >= 0.5) {
        return { color: '#F59E0B', style: 'dashed', dashPattern: [6, 3] }
      } else {
        return { color: '#9CA3AF', style: 'dashed', dashPattern: [2, 3] }
      }
    }

    // Build Cytoscape elements from topology data
    const elements: any[] = []

    // Add nodes
    topology.nodes?.forEach((node: any) => {
      elements.push({
        data: {
          id: node.id,
          label: node.label,
          type: node.type,
          status: node.status,
          discovered: node.discovered,
          ip_address: node.ip_address,
          site_name: node.site_name,
          manufacturer: node.manufacturer
        },
        classes: node.discovered ? 'discovered' : node.status
      })
    })

    // Add edges
    topology.edges?.forEach((edge: any) => {
      // Build edge label with interface names
      const sourceLabel = edge.source_interface || ''
      const targetLabel = edge.target_interface || ''
      const label = [sourceLabel, targetLabel].filter(Boolean).join(' \u2194 ')

      elements.push({
        data: {
          id: edge.id,
          source: edge.source,
          target: edge.target,
          source_interface: edge.source_interface,
          target_interface: edge.target_interface,
          link_type: edge.link_type,
          confidence: edge.confidence ?? 0.5,
          label: label
        }
      })
    })

    // Initialize Cytoscape
    this.cy = cytoscape({
      container: this.el,
      elements: elements,
      style: [
        {
          selector: 'node',
          style: {
            'background-color': function(ele: any) {
              return roleColors[ele.data('type')] || roleColors.unknown
            },
            'shape': function(ele: any) {
              return roleShapes[ele.data('type')] || roleShapes.unknown
            },
            'label': 'data(label)',
            'color': isDark ? '#fff' : '#000',
            'text-valign': 'bottom',
            'text-halign': 'center',
            'text-margin-y': 5,
            'font-size': '11px',
            'font-weight': '500',
            'width': 40,
            'height': 40,
            'border-width': 2,
            'border-color': function(ele: any) {
              const status = ele.data('status')
              if (status === 'down') return '#dc2626'     // darker red
              if (status === 'unknown') return '#ca8a04'   // darker yellow
              // Darken the role color for the border
              const type = ele.data('type')
              const darkerColors: Record<string, string> = {
                router: '#2563EB',
                switch: '#059669',
                wireless: '#7C3AED',
                firewall: '#DC2626',
                server: '#D97706',
                unknown: '#6B7280'
              }
              return darkerColors[type] || darkerColors.unknown
            },
            'overlay-color': function(ele: any) {
              const status = ele.data('status')
              if (status === 'down') return '#ef4444'
              return '#000'
            },
            'overlay-opacity': function(ele: any) {
              const status = ele.data('status')
              if (status === 'down') return 0.15
              return 0
            }
          } as any
        },
        {
          selector: 'node[status = "down"]',
          style: {
            'border-width': 3,
            'border-color': '#dc2626',
            'background-blacken': -0.3
          }
        },
        {
          selector: 'node.discovered',
          style: {
            'border-style': 'dashed',
            'border-width': 2,
            'border-color': '#9ca3af',
            'opacity': 0.6,
            'width': 30,
            'height': 30,
            'font-size': '9px'
          }
        },
        {
          selector: 'node:selected',
          style: {
            'border-width': 4,
            'border-color': '#fbbf24'
          }
        },
        {
          selector: 'edge',
          style: {
            'width': 2,
            'line-color': function(ele: any) {
              return getEdgeStyle(ele.data('confidence')).color
            },
            'line-style': function(ele: any) {
              return getEdgeStyle(ele.data('confidence')).style
            },
            'line-dash-pattern': function(ele: any) {
              const pattern = getEdgeStyle(ele.data('confidence')).dashPattern
              return pattern.length > 0 ? pattern : [1, 0]
            },
            'target-arrow-color': function(ele: any) {
              return getEdgeStyle(ele.data('confidence')).color
            },
            'curve-style': 'bezier',
            'label': 'data(label)',
            'font-size': '8px',
            'color': isDark ? '#d1d5db' : '#4b5563',
            'text-background-color': isDark ? '#1f2937' : '#ffffff',
            'text-background-opacity': 0.85,
            'text-background-padding': '2px',
            'text-rotation': 'autorotate',
            'text-margin-y': -8,
            'text-wrap': 'wrap',
            'text-max-width': '120px'
          } as any
        },
        {
          selector: 'edge:selected',
          style: {
            'line-color': '#fbbf24',
            'width': 3
          }
        }
      ],
      layout: {
        name: 'cose',
        animate: true,
        animationDuration: 500,
        nodeRepulsion: 30000,  // Even more repulsion to spread nodes apart
        idealEdgeLength: 250,  // Longer edges to give more room for labels
        edgeElasticity: 200,   // More flexible edges
        nestingFactor: 1,      // Less nesting
        gravity: 30,           // Even less gravity to allow more spread
        numIter: 2000,         // More iterations for better layout
        initialTemp: 500,      // Higher initial temperature
        coolingFactor: 0.95,
        minTemp: 1.0,
        nodeOverlap: 50,       // More overlap prevention
        avoidOverlap: true     // Enable overlap avoidance
      },
      minZoom: 0.2,
      maxZoom: 3,
      wheelSensitivity: 0.2
    })

    // Add click handler for nodes
    this.cy.on('tap', 'node', (evt: any) => {
      const node = evt.target
      const nodeData = node.data()

      // Push event to LiveView with node info
      this.pushEvent("node_clicked", { node_id: nodeData.id })

      // Show tooltip or navigate
      if (!nodeData.discovered) {
        // Navigate to device detail page
        window.location.href = `/devices/${nodeData.id}`
      }
    })

    // Add hover tooltips
    this.cy.on('mouseover', 'node', (evt: any) => {
      const node = evt.target
      const nodeData = node.data()

      let tooltip = nodeData.label
      if (nodeData.ip_address) tooltip += `\n${nodeData.ip_address}`
      if (nodeData.site_name) tooltip += `\n${nodeData.site_name}`
      if (nodeData.manufacturer) tooltip += `\n${nodeData.manufacturer}`

      node.style('label', tooltip)
    })

    this.cy.on('mouseout', 'node', (evt: any) => {
      const node = evt.target
      node.style('label', node.data('label'))
    })

    // Add hover for edges
    this.cy.on('mouseover', 'edge', (evt: any) => {
      const edge = evt.target
      const data = edge.data()

      // Build detailed tooltip with interfaces and link type
      let tooltip = ''
      if (data.source_interface) tooltip += `${data.source_interface}`
      if (data.target_interface) tooltip += ` \u2194 ${data.target_interface}`
      if (data.link_type) tooltip += `\n${data.link_type.toUpperCase()}`
      const confidence = data.confidence
      if (confidence != null) tooltip += ` (${Math.round(confidence * 100)}%)`

      edge.style({
        'line-color': '#fbbf24',
        'width': 3,
        'label': tooltip
      })
    })

    this.cy.on('mouseout', 'edge', (evt: any) => {
      const edge = evt.target
      const data = edge.data()
      const edgeStyle = getEdgeStyle(data.confidence ?? 0.5)
      edge.style({
        'line-color': edgeStyle.color,
        'width': 2,
        'label': data.label || ''
      })
    })
  },

  updateTopology(this: any, topology: any) {
    if (!this.cy) return

    // Rebuild the graph with new data
    this.cy.destroy()
    this.initializeCytoscape(topology)
  }
}

// MikroTik Port Sync - automatically switches port when SSL checkbox is toggled
const MikrotikPortSync = {
  handleSslChange: null as ((e: Event) => void) | null,
  sslCheckbox: null as HTMLInputElement | null,

  setupListener(this: any) {
    // Clean up existing listener first
    if (this.sslCheckbox && this.handleSslChange) {
      this.sslCheckbox.removeEventListener('change', this.handleSslChange)
    }

    const sslCheckbox = this.el.querySelector('[name="device[mikrotik_use_ssl]"]') as HTMLInputElement
    const portInput = this.el.querySelector('[name="device[mikrotik_port]"]') as HTMLInputElement

    if (!sslCheckbox || !portInput) return

    this.sslCheckbox = sslCheckbox

    this.handleSslChange = () => {
      const isChecked = sslCheckbox.checked
      const currentPort = portInput.value.trim()

      // Only update port if it's empty or set to one of the default values
      if (currentPort === '' || currentPort === '8729' || currentPort === '8728') {
        portInput.value = isChecked ? '8729' : '8728'
        // Trigger a change event so LiveView validates the new value
        portInput.dispatchEvent(new Event('input', { bubbles: true }))
      }
    }

    sslCheckbox.addEventListener('change', this.handleSslChange)
  },

  mounted(this: any) {
    this.setupListener()
  },

  updated(this: any) {
    // Re-attach listener when LiveView updates the DOM (e.g., when mikrotik_enabled changes)
    this.setupListener()
  },

  destroyed(this: any) {
    if (this.sslCheckbox && this.handleSslChange) {
      this.sslCheckbox.removeEventListener('change', this.handleSslChange)
      this.handleSslChange = null
      this.sslCheckbox = null
    }
  }
}

const csrfToken = document.querySelector<HTMLMetaElement>("meta[name='csrf-token']")?.getAttribute("content")
if (!csrfToken) {
  throw new Error('CSRF token meta tag not found')
}

// Detect user's timezone
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"

// Global Search (Cmd+K / Ctrl+K) hook
const GlobalSearch = {
  mounted() {
    this.handleKeydown = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === "k") {
        e.preventDefault()
        this.pushEventTo(this.el, "open", {})
      }
      if (e.key === "Escape" && this.el.querySelector("[role=dialog]")) {
        this.pushEventTo(this.el, "close", {})
      }
    }
    document.addEventListener("keydown", this.handleKeydown)
  },
  updated() {
    const input = this.el.querySelector("#global-search-input") as HTMLInputElement
    if (input) input.focus()
  },
  destroyed() {
    document.removeEventListener("keydown", this.handleKeydown)
  }
}

const liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: { _csrf_token: csrfToken, timezone: userTimezone },
  hooks: { ...colocatedHooks, SensorChart, CopyToClipboard, ScrollToTop, AutoDismissFlash, BetaBannerDismiss, NetworkMap, DeviceListReorder, MikrotikPortSync, GlobalSearch },
})

// Show progress bar on live navigation and form submits
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => {
  topbar.hide()
  // Re-check cookie consent on LiveView navigation
  initCookieConsent()
})

// Handle clipboard copy events
window.addEventListener("phx:copy", (event: any) => {
  // When using JS.dispatch with 'to:', the event is dispatched to that element
  // and event.target will be that element
  const el = event.target as HTMLElement

  let text = ""

  if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
    text = (el as HTMLInputElement | HTMLTextAreaElement).value
  } else {
    text = el.innerText || el.textContent || ""
  }

  if (text) {
    navigator.clipboard.writeText(text).then(() => {
      console.log('Copied to clipboard:', text.substring(0, 50) + (text.length > 50 ? '...' : ''))
    }).catch(err => {
      console.error('Failed to copy text:', err)
    })
  } else {
    console.warn('No text found to copy from element:', el)
  }
})

// Handle file download events
window.addEventListener("phx:download", (event: any) => {
  const { content, filename, mime_type } = event.detail

  // Create a blob from the content
  const blob = new Blob([content], { type: mime_type })

  // Create a temporary download link
  const url = window.URL.createObjectURL(blob)
  const link = document.createElement('a')
  link.href = url
  link.download = filename

  // Trigger the download
  document.body.appendChild(link)
  link.click()

  // Clean up
  document.body.removeChild(link)
  window.URL.revokeObjectURL(url)
})

// connect if there are any LiveViews on the page
liveSocket.connect()

// Initialize cookie consent handler
document.addEventListener('DOMContentLoaded', () => {
  initCookieConsent()
})

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
declare global {
  interface Window {
    liveSocket: LiveSocket
    liveReloader?: any
  }
}

window.liveSocket = liveSocket

// The lines below enable quality of life phoenix_live_reload
// development features:
//
//     1. stream server logs to the browser console
//     2. click on elements to jump to their definitions in your code editor
//
if (process.env.NODE_ENV === "development") {
  window.addEventListener("phx:live_reload:attached", ({ detail: reloader }: any) => {
    // Enable server log streaming to client.
    // Disable with reloader.disableServerLogs()
    reloader.enableServerLogs()

    // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
    //
    //   * click with "c" key pressed to open at caller location
    //   * click with "d" key pressed to open at function component definition location
    let keyDown: string | null = null
    window.addEventListener("keydown", e => keyDown = e.key)
    window.addEventListener("keyup", _e => keyDown = null)
    window.addEventListener("click", e => {
      if (keyDown === "c") {
        e.preventDefault()
        e.stopImmediatePropagation()
        reloader.openEditorAtCaller(e.target)
      } else if (keyDown === "d") {
        e.preventDefault()
        e.stopImmediatePropagation()
        reloader.openEditorAtDef(e.target)
      }
    }, true)

    window.liveReloader = reloader
  })
}
