import log from "./utils/log.ts"
import { Emitter } from "./utils/emitter.ts"
import { processStdout, processStderr } from "./utils/stdio.ts"

export class Baresip {
  baresipArgs: string[]
  process?: Bun.PipedSubprocess
  registered = false
  callEstablished = new Emitter<{ contact: string }>()
  callReceived = new Emitter<{ contact: string }>()
  hungUp = new Emitter()
  dialFailed = new Emitter<{ reason: string }>()
  error = new Emitter<{ message: string; statusCode?: string; reason?: string }>()
  registrationSuccess = new Emitter()

  constructor(baresipArgs: string[]) {
    this.baresipArgs = baresipArgs
  }

  async connect() {
    this.process = Bun.spawn(this.baresipArgs, {
      stdout: "pipe",
      stderr: "pipe",
      onExit: (_proc, exitCode, signalCode, error) => {
        log.debug(`📞 Baresip process exited (code: ${exitCode}, signal: ${signalCode})`)
        if (error) {
          log.error("Process error:", error)
        }
      },
    })

    Promise.all([
      processStdout(this.process, (line) => this.#parseLine(line)),
      processStderr(this.process),
    ]).catch((error) => {
      log.error("Error processing output:", error)
    })
  }

  accept() {
    executeCommand("a")
  }

  async dial(phoneNumber: string) {
    if (!this.registered) {
      this.dialFailed.emit({ reason: "Not registered with SIP server" })
      return
    }
    const success = await executeCommand(`d${phoneNumber}`)
    if (!success) {
      this.dialFailed.emit({ reason: "Command timed out" })
    }
  }

  hangUp() {
    executeCommand("b")
  }

  disconnectAll() {
    this.callEstablished.removeAllListeners()
    this.callReceived.removeAllListeners()
    this.hungUp.removeAllListeners()
    this.registrationSuccess.removeAllListeners()
    this.dialFailed.removeAllListeners()
    this.error.removeAllListeners()
  }

  kill() {
    if (!this.process) throw new Error("Process not started")
    this.process.kill()
    this.disconnectAll()
    this.process = undefined
  }

  async restart() {
    this.registered = false
    if (this.process) {
      this.process.kill()
      this.process = undefined
    }
    await this.connect()
  }

  #parseLine(line: string) {
    log.debug(`📞 Baresip: ${line}`)
    const callEstablishedMatch = line.match(/Call established: (.+)/)
    if (callEstablishedMatch) {
      log.debug(`Call established with "${line}"`)
      this.callEstablished.emit({ contact: callEstablishedMatch[1]! })
    }

    const callReceivedMatch = line.match(/Incoming call from: \+\d+ (\S+) -/)
    if (callReceivedMatch) {
      log.debug(`Incoming call from "${line}"`)
      this.callReceived.emit({ contact: callReceivedMatch[1]!?.trim() })
    }

    const hangUpMatch = line.match(/(.+): session closed/)
    if (hangUpMatch) {
      log.debug(`Call hung up with "${line}"`)
      this.hungUp.emit()
    }

    const callTerminatedMatch = line.match(/(.+) terminated \(duration: /)
    if (callTerminatedMatch) {
      log.debug(`⁉️ NOT HANDLED: Call terminated with "${line}"`)
    }

    const registrationSuccessMatch = line.match(/\[\d+ bindings?\]/)
    if (registrationSuccessMatch) {
      this.registered = true
      this.registrationSuccess.emit()
    }

    const registrationFailedMatch = line.match(/reg: sip:\S+ .*?(\d{3}) (\w+)/)
    const socketInUseMatch = line.match(/tcp: sock_bind:/)
    if (registrationFailedMatch) {
      const [, statusCode, reason] = registrationFailedMatch
      log.error(`Registration failed: ${statusCode} ${reason}`)
      this.registered = false
      this.error.emit({ message: line, statusCode, reason })
    } else if (socketInUseMatch) {
      log.error(`Registration failed: socket in use`)
      this.registered = false
      this.error.emit({ message: line })
    }
  }
}

const executeCommand = async (command: string): Promise<boolean> => {
  try {
    const url = new URL(`/?${command}`, "http://127.0.0.1:8000")
    const controller = new AbortController()
    const timeout = setTimeout(() => controller.abort(), 10000)

    const response = await fetch(url, { signal: controller.signal })
    clearTimeout(timeout)

    if (!response.ok) {
      throw new Error(`Error executing command: ${response.statusText}`)
    }
    return true
  } catch (error) {
    log.error("Failed to execute command:", error)
    return false
  }
}
