const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const { spawn } = require('node:child_process')
const CDP = require('chrome-remote-interface')

class ChromeAdapter {
  constructor(options = {}) {
    this.port = options.port || 9222
    this.host = options.host || '127.0.0.1'
    this.process = undefined
    this.userDataDir = options.userDataDir || path.join(os.tmpdir(), 'hyper-desk-chrome-profile')
  }

  async launch(url = 'about:blank') {
    if (await this.isAvailable()) {
      if (url && url !== 'about:blank') await this.navigate(url)
      return `Connected to Chrome DevTools on ${this.host}:${this.port}`
    }

    fs.mkdirSync(this.userDataDir, { recursive: true })
    const executable = this.findChromeExecutable()
    const args = [
      `--remote-debugging-port=${this.port}`,
      `--user-data-dir=${this.userDataDir}`,
      '--no-first-run',
      '--no-default-browser-check',
      '--new-window',
      url || 'about:blank'
    ]

    this.process = spawn(executable, args, { detached: true, stdio: 'ignore' })
    this.process.unref()
    await this.waitUntilAvailable()
    return `Launched visible Chrome with DevTools on ${this.host}:${this.port}`
  }

  findChromeExecutable() {
    if (process.platform === 'darwin') {
      const candidates = [
        '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
        '/Applications/Chromium.app/Contents/MacOS/Chromium',
        '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'
      ]
      const found = candidates.find(candidate => fs.existsSync(candidate))
      if (found) return found
    }

    if (process.platform === 'win32') {
      const candidates = [
        path.join(process.env.PROGRAMFILES || '', 'Google/Chrome/Application/chrome.exe'),
        path.join(process.env['PROGRAMFILES(X86)'] || '', 'Google/Chrome/Application/chrome.exe'),
        path.join(process.env.LOCALAPPDATA || '', 'Google/Chrome/Application/chrome.exe')
      ]
      const found = candidates.find(candidate => fs.existsSync(candidate))
      if (found) return found
    }

    return process.platform === 'win32' ? 'chrome.exe' : 'google-chrome'
  }

  async isAvailable() {
    try {
      await this.listTabs()
      return true
    } catch (_) {
      return false
    }
  }

  async waitUntilAvailable() {
    const deadline = Date.now() + 8000
    while (Date.now() < deadline) {
      if (await this.isAvailable()) return
      await new Promise(resolve => setTimeout(resolve, 200))
    }
    throw new Error(`Chrome DevTools did not become available on ${this.host}:${this.port}`)
  }

  async newTab(url = 'about:blank') {
    if (!(await this.isAvailable())) await this.launch('about:blank')
    const target = await CDP.New({ host: this.host, port: this.port, url: this.normalizeUrl(url) })
    await CDP.Activate({ host: this.host, port: this.port, id: target.id })
    return `Opened new Chrome tab: ${target.url || this.normalizeUrl(url)}`
  }

  async listTabs() {
    const tabs = await CDP.List({ host: this.host, port: this.port })
    return tabs
      .filter(tab => tab.type === 'page')
      .map(tab => ({ id: tab.id, title: tab.title, url: tab.url, type: tab.type }))
  }

  async getTarget(tabId) {
    const tabs = await CDP.List({ host: this.host, port: this.port })
    const pages = tabs.filter(tab => tab.type === 'page')
    if (tabId) {
      const target = pages.find(tab => tab.id === tabId)
      if (!target) throw new Error(`No Chrome tab found with id ${tabId}`)
      return target
    }
    if (pages[0]) return pages[0]
    return await CDP.New({ host: this.host, port: this.port, url: 'about:blank' })
  }

  async withClient(tabId, fn) {
    const target = await this.getTarget(tabId)
    const client = await CDP({ host: this.host, port: this.port, target })
    try {
      return await fn(client, target)
    } finally {
      await client.close()
    }
  }

  async navigate(url, tabId) {
    const normalized = this.normalizeUrl(url)
    return await this.withClient(tabId, async ({ Page }, target) => {
      await Page.enable()
      await CDP.Activate({ host: this.host, port: this.port, id: target.id })
      await Page.navigate({ url: normalized })
      return `Navigated Chrome tab to ${normalized}`
    })
  }

  async snapshotText(maxChars = 12000, tabId) {
    const limit = this.limit(maxChars, 1, 50000, 12000)
    return await this.withClient(tabId, async ({ Runtime }) => {
      const result = await Runtime.evaluate({
        expression: `(document.body?.innerText || '').slice(0, ${limit})`,
        returnByValue: true
      })
      return result.result.value || ''
    })
  }

  async snapshotElements(maxElements = 100, tabId) {
    const limit = this.limit(maxElements, 1, 300, 100)
    const expression = `(() => Array.from(document.querySelectorAll('a,button,input,textarea,select,[role="button"],[contenteditable="true"]'))
      .filter(el => {
        const rect = el.getBoundingClientRect();
        const style = getComputedStyle(el);
        return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
      })
      .slice(0, ${limit})
      .map((el, index) => {
        const rect = el.getBoundingClientRect();
        return {
          index,
          tag: el.tagName.toLowerCase(),
          type: el.getAttribute('type') || '',
          role: el.getAttribute('role') || '',
          text: (el.innerText || el.value || el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.getAttribute('title') || '').trim().slice(0, 240),
          href: el.href || '',
          disabled: Boolean(el.disabled || el.getAttribute('aria-disabled') === 'true'),
          rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) }
        };
      }))()`

    return await this.withClient(tabId, async ({ Runtime }) => {
      const result = await Runtime.evaluate({ expression, returnByValue: true })
      return result.result.value || []
    })
  }

  async scroll(x = 0, y = 600, tabId) {
    const safeX = Number.isFinite(Number(x)) ? Number(x) : 0
    const safeY = Number.isFinite(Number(y)) ? Number(y) : 600
    return await this.withClient(tabId, async ({ Runtime }) => {
      await Runtime.evaluate({
        expression: `window.scrollBy({ left: ${safeX}, top: ${safeY}, behavior: 'smooth' })`
      })
      return `Scrolled Chrome by ${safeY}px`
    })
  }

  async click(index, tabId) {
    const safeIndex = this.limit(index, 0, 10000, 0)
    return await this.withClient(tabId, async ({ Runtime }) => {
      await Runtime.evaluate({ expression: this.elementScript(safeIndex, `el.scrollIntoView({ block: 'center', inline: 'center' }); el.click();`) })
      return `Clicked Chrome element #${safeIndex}`
    })
  }

  async fill(index, value, tabId) {
    const safeIndex = this.limit(index, 0, 10000, 0)
    const safeValue = JSON.stringify(String(value || ''))
    return await this.withClient(tabId, async ({ Runtime }) => {
      await Runtime.evaluate({
        expression: this.elementScript(safeIndex, `
          el.scrollIntoView({ block: 'center', inline: 'center' });
          el.focus();
          if (el.isContentEditable) el.textContent = ${safeValue};
          else el.value = ${safeValue};
          el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: ${safeValue} }));
          el.dispatchEvent(new Event('change', { bubbles: true }));
        `)
      })
      return `Filled Chrome element #${safeIndex}`
    })
  }

  async pressKey(key, tabId) {
    const safeKey = String(key || 'Enter')
    return await this.withClient(tabId, async ({ Input }, target) => {
      await CDP.Activate({ host: this.host, port: this.port, id: target.id })
      await Input.dispatchKeyEvent({ type: 'keyDown', key: safeKey })
      await Input.dispatchKeyEvent({ type: 'keyUp', key: safeKey })
      return `Pressed ${safeKey} in Chrome`
    })
  }

  elementScript(index, body) {
    return `(() => {
      const elements = Array.from(document.querySelectorAll('a,button,input,textarea,select,[role="button"],[contenteditable="true"]'))
        .filter(el => {
          const rect = el.getBoundingClientRect();
          const style = getComputedStyle(el);
          return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
        });
      const el = elements[${index}];
      if (!el) throw new Error('No element at index ${index}');
      ${body}
    })()`
  }

  normalizeUrl(url) {
    const value = String(url || '').trim()
    if (value === 'about:blank') return value
    if (value.startsWith('http://') || value.startsWith('https://')) return value
    return `https://${value}`
  }

  limit(value, min, max, fallback) {
    const number = Number(value)
    if (!Number.isFinite(number)) return fallback
    return Math.max(min, Math.min(max, Math.floor(number)))
  }
}

module.exports = { ChromeAdapter }
