import { ISLANDER_CONFIG } from './config'
import type { PageElementSnapshot, PageSnapshot } from './types'

function resolveRoot(): HTMLElement {
  for (const selector of ISLANDER_CONFIG.rootSelectors) {
    const el = document.querySelector<HTMLElement>(selector)
    if (el) return el
  }
  return document.body
}

function isVisible(el: HTMLElement) {
  const rect = el.getBoundingClientRect()
  if (rect.width < 2 || rect.height < 2) return false
  const style = window.getComputedStyle(el)
  if (style.visibility === 'hidden' || style.display === 'none' || style.opacity === '0') return false
  return true
}

function inViewport(el: HTMLElement) {
  const rect = el.getBoundingClientRect()
  return rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth
}

function cleanText(value: string) {
  return value.replace(/\s+/g, ' ').trim()
}

function labelForElement(el: HTMLElement) {
  const explicit = el.getAttribute(ISLANDER_CONFIG.labelAttr)
  if (explicit) return cleanText(explicit)

  const aria = el.getAttribute('aria-label')
  if (aria) return cleanText(aria)

  const fieldLabel = el.closest('label')?.querySelector('span')?.textContent
  if (fieldLabel) return cleanText(fieldLabel)

  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
    const placeholder = el.getAttribute('placeholder')
    if (placeholder) return cleanText(placeholder)
    const id = el.id
    if (id) {
      const linked = document.querySelector<HTMLLabelElement>(`label[for="${CSS.escape(id)}"]`)
      if (linked?.textContent) return cleanText(linked.textContent)
    }
    const name = el.getAttribute('name')
    if (name) return cleanText(name)
  }

  const text = cleanText(el.textContent ?? '')
  if (text) return text.slice(0, 80)

  const title = el.getAttribute('title')
  if (title) return cleanText(title)

  return el.tagName.toLowerCase()
}

function metaForElement(el: HTMLElement) {
  if (el instanceof HTMLAnchorElement && el.href) {
    const url = new URL(el.href, window.location.origin)
    if (url.origin === window.location.origin) return url.pathname + url.search
    return el.getAttribute('href') ?? undefined
  }
  if (el instanceof HTMLInputElement) {
    const type = el.type || 'text'
    const bits = [`type=${type}`]
    if (el.value && type !== 'password') bits.push(`value=${el.value.slice(0, 40)}`)
    return bits.join(' ')
  }
  if (el instanceof HTMLTextAreaElement && el.value) {
    return `value=${el.value.slice(0, 40)}`
  }
  if (el instanceof HTMLSelectElement) {
    return `selected=${el.value || '(none)'}`
  }
  if (el instanceof HTMLButtonElement && el.type) {
    return `type=${el.type}`
  }
  return undefined
}

function kindForElement(el: HTMLElement) {
  const explicit = el.getAttribute(ISLANDER_CONFIG.kindAttr)
  if (explicit) return cleanText(explicit)

  if (el instanceof HTMLAnchorElement) return 'link'
  if (el instanceof HTMLButtonElement) return 'button'
  if (el instanceof HTMLTextAreaElement) return 'textarea'
  if (el instanceof HTMLSelectElement) return 'select'
  if (el instanceof HTMLInputElement) return `input:${el.type || 'text'}`
  const role = el.getAttribute('role')
  if (role) return role
  return el.tagName.toLowerCase()
}

function isInteractive(el: HTMLElement) {
  const exclude = ISLANDER_CONFIG.excludeSelectors.join(', ')
  if (el.closest(exclude)) return false
  if (el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true') return false
  if (el instanceof HTMLInputElement && (el.type === 'hidden' || el.type === 'file')) return false
  return true
}

function collectCandidates(root: ParentNode) {
  const nodes = root.querySelectorAll<HTMLElement>(ISLANDER_CONFIG.interactiveSelectors.join(', '))
  const unique = new Set<HTMLElement>()
  for (const node of nodes) {
    if (!(node instanceof HTMLElement)) continue
    if (!isInteractive(node)) continue
    if (!isVisible(node)) continue
    unique.add(node)
  }
  return [...unique]
}

function clearPreviousRefs(root: ParentNode) {
  const attr = ISLANDER_CONFIG.refAttr
  root.querySelectorAll<HTMLElement>(`[${attr}]`).forEach((el) => el.removeAttribute(attr))
}

export function capturePageSnapshot(): PageSnapshot {
  const root = resolveRoot()
  clearPreviousRefs(root)

  const candidates = collectCandidates(root)
  const sorted = candidates.sort((a, b) => {
    const av = inViewport(a) ? 0 : 1
    const bv = inViewport(b) ? 0 : 1
    if (av !== bv) return av - bv
    const ar = a.getBoundingClientRect()
    const br = b.getBoundingClientRect()
    return ar.top - br.top || ar.left - br.left
  })

  const refMap = new Map<string, HTMLElement>()
  const elements: PageElementSnapshot[] = []

  sorted.slice(0, ISLANDER_CONFIG.maxElements).forEach((el, index) => {
    const ref = `hx-${index + 1}`
    el.setAttribute(ISLANDER_CONFIG.refAttr, ref)
    refMap.set(ref, el)
    elements.push({
      ref,
      kind: kindForElement(el),
      label: labelForElement(el),
      meta: metaForElement(el),
      inView: inViewport(el),
    })
  })

  return { elements, refMap }
}

export function formatPageSnapshot(snapshot: PageSnapshot): string {
  if (snapshot.elements.length === 0) {
    return '（此頁暫無可互動元素，或頁面尚在載入）'
  }

  return snapshot.elements
    .map((item) => {
      const bits = [`${item.ref}`, item.kind, item.label]
      if (item.meta) bits.push(item.meta)
      if (!item.inView) bits.push('offscreen')
      return `- ${bits.join(' | ')}`
    })
    .join('\n')
}

export function resolveSnapshotElement(refMap: Map<string, HTMLElement>, ref: string) {
  const attr = ISLANDER_CONFIG.refAttr
  const el = refMap.get(ref) ?? document.querySelector<HTMLElement>(`[${attr}="${ref}"]`)
  return el ?? null
}

export function findElementByLabel(label: string): HTMLElement | null {
  const target = cleanText(label)
  if (!target) return null

  const root = resolveRoot()
  const attr = ISLANDER_CONFIG.labelAttr

  const explicit = root.querySelector<HTMLElement>(`[${attr}="${CSS.escape(target)}"]`)
  if (explicit && isInteractive(explicit) && isVisible(explicit)) return explicit

  const targetLower = target.toLowerCase()
  for (const el of collectCandidates(root)) {
    const elLabel = labelForElement(el)
    const normalized = cleanText(elLabel).toLowerCase()
    if (normalized === targetLower || normalized.includes(targetLower)) return el
  }

  return null
}