const { InstanceBase, Regex, runEntrypoint, InstanceStatus } = require('@companion-module/base')
const UpgradeScripts = require('./upgrades')
const UpdateActions = require('./actions')
const UpdateFeedbacks = require('./feedbacks')
const UpdateVariableDefinitions = require('./variables')
const net = require('net')

class ModuleInstance extends InstanceBase {
  // --- helpers / state ---
  reconnectTimer = null
  socket = null

  hasValidConfig(cfg = this.config) {
    const hostOk = !!cfg?.host && String(cfg.host).trim().length > 0
    const portNum = Number(cfg?.port)
    const portOk = Number.isInteger(portNum) && portNum > 0 && portNum < 65536
    return hostOk && portOk
  }

  clearReconnectTimer() {
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer)
      this.reconnectTimer = null
    }
  }

  scheduleReconnect(delayMs = 5000) {
    this.clearReconnectTimer()
    this.reconnectTimer = setTimeout(() => {
      this.createSocketAndConnect()
    }, delayMs)
  }

  async init(config) {
    this.config = config

    // export actions/feedbacks/vars
    this.updateActions()
    this.updateFeedbacks()
    this.updateVariableDefinitions()

    if (this.hasValidConfig()) {
      this.createSocketAndConnect()
    } else {
      this.updateStatus(InstanceStatus.BadConfig, 'Please set a valid Host and Port.')
    }
  }

  // When module gets deleted
  async destroy() {
    this.log('debug', 'destroy')
    this.clearReconnectTimer()
    if (this.socket) {
      try {
        this.socket.destroy()
      } catch (_) {}
      this.socket = null
    }
  }

  sendAppMessage(message) {
    if (!this.socket || this.socket.destroyed) {
      this.log('error', `Socket not connected, can't send: ${JSON.stringify(message)}`)
      return
    }
    try {
      const json = JSON.stringify(message)
      const payload = 'companionSend:' + json
      this.socket.write(payload + '\n')
      this.log('debug', `TX: ${payload}`)
    } catch (e) {
      this.log('error', `Failed to send message: ${e?.message || e}`)
    }
  }

  sendRaw(text) {
    if (!this.socket || this.socket.destroyed) {
      this.log('error', `Socket not connected, can't send raw: ${text}`)
      return
    }
    const payload = String(text)
    this.socket.write(payload.endsWith('\n') ? payload : payload + '\n')
    this.log('debug', `TX(raw): ${payload}`)
  }

  async configUpdated(config) {
    const old = this.config || {}
    this.config = config

    const hostChanged = old.host !== config.host
    const portChanged = String(old.port) !== String(config.port)

    if (!this.hasValidConfig()) {
      this.updateStatus(InstanceStatus.BadConfig, 'Please set a valid Host and Port.')
      if (this.socket) {
        try { this.socket.destroy() } catch (_) {}
        this.socket = null
      }
      this.clearReconnectTimer()
      return
    }

    if (hostChanged || portChanged) {
      this.log('info', `Config changed: reconnecting to ${config.host}:${config.port}`)
      this.clearReconnectTimer()
      if (this.socket) {
        try { this.socket.destroy() } catch (_) {}
        this.socket = null
      }
      this.createSocketAndConnect()
    }
  }

  // Return config fields for web config
  getConfigFields() {
    return [
      {
        type: 'textinput',
        id: 'host',
        label: 'Target IP / Host',
        width: 8,
        // allow IP or hostname (e.g., localhost)
        regex: Regex.IP_OR_HOST,
        default: 'localhost',
      },
      {
        type: 'textinput',
        id: 'port',
        label: 'Target Port',
        width: 4,
        regex: Regex.PORT,
        default: '6000',
      },
    ]
  }

  updateActions() {
    UpdateActions(this)
  }

  updateFeedbacks() {
    UpdateFeedbacks(this)
  }

  updateVariableDefinitions() {
    UpdateVariableDefinitions(this)
  }

  createSocketAndConnect() {
    if (!this.hasValidConfig()) {
      this.updateStatus(InstanceStatus.BadConfig, 'Please set a valid Host and Port.')
      return
    }

    const host = String(this.config.host).trim()
    const port = Number(this.config.port)

    // cleanup any previous socket
    if (this.socket) {
      try { this.socket.destroy() } catch (_) {}
      this.socket = null
    }
    this.clearReconnectTimer()

    this.updateStatus(InstanceStatus.Connecting, `Connecting to ${host}:${port} ...`)

    const sock = new net.Socket()
    this.socket = sock

    try {
      sock.setKeepAlive(true, 10_000)
      sock.setTimeout(12_000)

      sock.on('timeout', () => {
        this.log('warn', 'Socket timeout, closing and scheduling reconnect')
        try { sock.destroy() } catch (_) {}
      })

      sock.on('error', (err) => {
        this.log('error', `Socket error: ${err.message}`)
        this.updateStatus(InstanceStatus.ConnectionFailure, err.message)
      })

      sock.on('close', (hadErr) => {
        this.log(hadErr ? 'error' : 'warn', 'Socket closed, will retry in 5s')
        this.scheduleReconnect(5000)
      })

      sock.on('data', (buf) => {
        const msg = buf.toString('utf8').trim()
        this.log('debug', `RX: ${msg}`)
        // TODO: parse and route to variables/feedbacks if needed
      })

      this.log('info', `Connecting to ${host}:${port}`)
      sock.connect(port, host, () => {
        this.log('info', `Connected to ${host}:${port}`)
        this.updateStatus(InstanceStatus.Ok)
      })
    } catch (err) {
      this.log('error', `Connect exception: ${err.message}`)
      this.updateStatus(InstanceStatus.ConnectionFailure, err.message)
      this.scheduleReconnect(5000)
    }
  }
}

runEntrypoint(ModuleInstance, UpgradeScripts)
