import browser from 'webextension-polyfill'
import { isLikelyDownloadResponse, normalizeUrl } from '../lib/downloadIntent'
import { nativeAddUri, nativeFocus } from '../lib/nativeHost'
import { getSettings } from '../lib/settings'
import { upsertHistory } from '../lib/history'
import { makeMediaCandidate, mediaFingerprint, shouldCaptureMediaResponse } from '../lib/mediaCapture'
import { clearMediaCandidates, listMediaCandidates, upsertMediaCandidate } from '../lib/mediaStore'

const REQUEST_TTL_MS = 8000
const TRANSFER_DEDUPE_TTL_MS = 7000
const contextMenuId = 'gomdown-helper-download-context-menu-option'

const pendingRequests = new Map<string, any>()
const capturedUrls = new Map<string, number>()
const capturedFingerprints = new Map<string, number>()
const recentTransferFingerprints = new Map<string, number>()
const handledRequestIds = new Map<string, number>()
const capturedTabIds = new Map<number, number>()
const recentMediaFingerprints = new Map<string, number>()
let webRequestHooked = false
let downloadHooked = false
let contextMenuHooked = false
let contextMenuUpdateInFlight: Promise<void> | null = null

function urlFingerprint(raw: string): string {
  try {
    const u = new URL(raw)
    const path = (u.pathname || '/').replace(/\/+$/, '') || '/'
    return `${u.protocol}//${u.host}${path}`.toLowerCase()
  } catch {
    return String(raw || '').toLowerCase()
  }
}

function pruneMap<K>(map: Map<K, number>): void {
  const now = Date.now()
  for (const [key, expiresAt] of map.entries()) {
    if (expiresAt <= now) map.delete(key)
  }
}

function rememberCapturedUrl(url: string): void {
  const expiresAt = Date.now() + REQUEST_TTL_MS
  capturedUrls.set(normalizeUrl(url), expiresAt)
  capturedFingerprints.set(urlFingerprint(url), expiresAt)
}

function rememberCapturedTab(tabId?: number): void {
  if (!Number.isInteger(tabId) || (tabId ?? -1) < 0) return
  capturedTabIds.set(tabId as number, Date.now() + REQUEST_TTL_MS)
}

function wasCapturedUrl(url: string): boolean {
  pruneMap(capturedUrls)
  pruneMap(capturedFingerprints)
  const normalized = normalizeUrl(url)
  return capturedUrls.has(normalized) || capturedFingerprints.has(urlFingerprint(url))
}

function wasCapturedTab(tabId?: number): boolean {
  pruneMap(capturedTabIds)
  if (!Number.isInteger(tabId) || (tabId ?? -1) < 0) return false
  return capturedTabIds.has(tabId as number)
}

function shouldSuppressDuplicateTransfer(url: string): boolean {
  pruneMap(recentTransferFingerprints)
  return recentTransferFingerprints.has(urlFingerprint(url))
}

function rememberRecentTransfer(url: string): void {
  recentTransferFingerprints.set(urlFingerprint(url), Date.now() + TRANSFER_DEDUPE_TTL_MS)
}

function wasRequestHandled(requestId?: string): boolean {
  pruneMap(handledRequestIds)
  return !!requestId && handledRequestIds.has(requestId)
}

function markRequestHandled(requestId?: string): void {
  if (!requestId) return
  handledRequestIds.set(requestId, Date.now() + REQUEST_TTL_MS)
}

function shouldSuppressDuplicateMedia(url: string): boolean {
  pruneMap(recentMediaFingerprints)
  const fp = mediaFingerprint(url)
  return recentMediaFingerprints.has(fp)
}

function rememberMediaFingerprint(url: string): void {
  recentMediaFingerprints.set(mediaFingerprint(url), Date.now() + REQUEST_TTL_MS)
}

async function requestGdownFocus(): Promise<void> {
  try {
    await nativeFocus()
  } catch {
    // ignored
  }
}

async function notify(message: string): Promise<void> {
  await browser.notifications
    .create(`gomdown-notice-${Date.now()}`, {
      type: 'basic',
      iconUrl: '/images/icon-large.png',
      title: 'Gomdown Helper',
      message,
    })
    .catch(() => null)
}

async function transferUrlToGdown(
  url: string,
  referer = '',
  extractor?: 'yt-dlp' | 'aria2',
  format?: string
): Promise<{ ok: boolean; error?: string }> {
  if (shouldSuppressDuplicateTransfer(url)) {
    return { ok: false, error: 'duplicate transfer suppressed' }
  }

  const settings = await getSettings()
  if (!settings.extensionStatus) return { ok: false, error: 'extension disabled' }
  if (!settings.motrixAPIkey) return { ok: false, error: 'motrixAPIkey is not set' }

  try {
    const nativeResult = await nativeAddUri({
      url,
      rpcPort: settings.motrixPort,
      rpcSecret: settings.motrixAPIkey,
      referer,
      split: 64,
      extractor: extractor === 'yt-dlp' ? 'yt-dlp' : undefined,
      format: extractor === 'yt-dlp' ? format || 'bestvideo*+bestaudio/best' : undefined,
    })

    if (!nativeResult?.ok) {
      return { ok: false, error: nativeResult?.error || 'native host addUri failed' }
    }

    if (settings.activateAppOnDownload) {
      await requestGdownFocus()
    }
    rememberRecentTransfer(url)
    const gid = String(nativeResult?.gid || nativeResult?.requestId || `pending-${Date.now()}`)
    const pathname = (() => {
      try {
        return new URL(url).pathname
      } catch {
        return ''
      }
    })()
    const guessedName = pathname.split('/').filter(Boolean).pop() || url
    await upsertHistory({
      gid,
      downloader: 'native',
      startTime: new Date().toISOString(),
      icon: '/images/32.png',
      name: decodeURIComponent(guessedName),
      path: null,
      status: nativeResult?.pending ? 'queued' : 'downloading',
      size: 0,
      downloaded: 0,
    })

    if (settings.enableNotifications) {
      await browser.notifications.create(`gomdown-transfer-${Date.now()}`, {
        type: 'basic',
        iconUrl: '/images/icon-large.png',
        title: 'Gomdown Helper',
        message: 'Download sent to gdown',
      })
    }

    return { ok: true }
  } catch (error) {
    return { ok: false, error: String(error) }
  }
}

async function shouldCaptureRequest(details: any): Promise<boolean> {
  if (details.type !== 'main_frame') return false
  if ((details.method || '').toUpperCase() !== 'GET') return false
  if (typeof details.statusCode === 'number' && (details.statusCode < 200 || details.statusCode > 299)) return false

  const settings = await getSettings()
  if (!settings.extensionStatus || !settings.motrixAPIkey) return false
  const contentLengthRaw = String(
    Array.isArray(details?.responseHeaders)
      ? details.responseHeaders.find((h: any) => String(h?.name || '').toLowerCase() === 'content-length')?.value || ''
      : ''
  )
  const contentLength = Number(contentLengthRaw || 0)
  if (settings.minFileSize > 0 && contentLength > 0 && contentLength < settings.minFileSize * 1024 * 1024) {
    return false
  }
  return isLikelyDownloadResponse(details)
}

async function interceptByWebRequest(details: any): Promise<void> {
  if (wasRequestHandled(details.requestId)) return

  const accepted = await shouldCaptureRequest(details)
  if (!accepted) return

  const req = pendingRequests.get(details.requestId)
  const referer = String(req?.documentUrl || req?.originUrl || req?.initiator || req?.url || '')
  const sent = await transferUrlToGdown(details.url, referer)
  if (!sent.ok) return

  markRequestHandled(details.requestId)
  rememberCapturedUrl(details.url)
  rememberCapturedTab(details.tabId)
}

async function interceptMediaByWebRequest(details: any): Promise<void> {
  if (!shouldCaptureMediaResponse(details)) return
  const settings = await getSettings()
  if (!settings.extensionStatus) return
  if (shouldSuppressDuplicateMedia(details.url)) return

  const req = pendingRequests.get(details.requestId)
  const referer = String(req?.documentUrl || req?.originUrl || req?.initiator || req?.url || '')
  const candidate = makeMediaCandidate(details, referer)
  await upsertMediaCandidate(candidate, mediaFingerprint(candidate.url))
  rememberMediaFingerprint(candidate.url)
}

async function applyShelfVisibility(): Promise<void> {
  const downloadsApi = browser.downloads as any
  if (!downloadsApi.setShelfEnabled) return
  const settings = await getSettings()
  if (!settings.extensionStatus) return
  const enabled = settings.useNativeHost ? false : !settings.hideChromeBar
  await downloadsApi.setShelfEnabled(enabled)
}

function setupDownloadCancelHook(): void {
  if (downloadHooked) return
  downloadHooked = true
  browser.downloads.onCreated.addListener(async (downloadItem) => {
    await applyShelfVisibility()
    const download = downloadItem as any
    const target = download.finalUrl || download.url || ''
    if (!wasCapturedUrl(target) && !wasCapturedTab(download.tabId)) return

    await browser.downloads.cancel(downloadItem.id).catch(() => null)
    await browser.downloads.erase({ id: downloadItem.id }).catch(() => null)
    await browser.downloads.removeFile(downloadItem.id).catch(() => null)
  })
}

function setupWebRequestInterceptor(): void {
  if (webRequestHooked) return
  webRequestHooked = true
  browser.webRequest.onSendHeaders.addListener(
    (details) => {
      pendingRequests.set(details.requestId, details as any)
    },
    { urls: ['<all_urls>'] },
    ['requestHeaders', 'extraHeaders']
  )

  browser.webRequest.onErrorOccurred.addListener(
    (details) => {
      pendingRequests.delete(details.requestId)
      handledRequestIds.delete(String(details.requestId))
    },
    { urls: ['<all_urls>'] }
  )

  browser.webRequest.onCompleted.addListener(
    (details) => {
      pendingRequests.delete(details.requestId)
      handledRequestIds.delete(String(details.requestId))
    },
    { urls: ['<all_urls>'] }
  )

  browser.webRequest.onHeadersReceived.addListener(
    (details) => {
      void interceptByWebRequest(details)
      void interceptMediaByWebRequest(details)
    },
    { urls: ['<all_urls>'] },
    ['responseHeaders']
  )
}

async function handleContextMenuClick(data: any, tab?: any): Promise<void> {
  console.log('[gomdown-helper] context menu clicked', {
    menuItemId: data?.menuItemId,
    linkUrl: data?.linkUrl,
    srcUrl: data?.srcUrl,
    frameUrl: data?.frameUrl,
    pageUrl: data?.pageUrl,
    tabUrl: tab?.url,
  })
  const clickedId = data?.menuItemId
  if (clickedId != null && String(clickedId) !== contextMenuId) return

  const linkUrl = String(data?.linkUrl || data?.srcUrl || '').trim()
  const pageUrl = String(data?.frameUrl || data?.pageUrl || tab?.url || '').trim()
  const targetUrl = linkUrl || pageUrl

  const url = String(targetUrl || '').trim()
  if (!url || /^(about:|chrome:|chrome-extension:|edge:|brave:)/i.test(url)) {
    await notify('다운로드 가능한 URL을 찾지 못했습니다.')
    return
  }

  const useYtDlp = !linkUrl && !!pageUrl
  const result = await transferUrlToGdown(
    url,
    String(data?.pageUrl || tab?.url || ''),
    useYtDlp ? 'yt-dlp' : 'aria2'
  )
  if (!result.ok) {
    await notify(`전송 실패: ${result.error || 'unknown error'}`)
    return
  }
  await notify(useYtDlp ? '페이지 URL을 yt-dlp로 gdown에 전송했습니다.' : 'gdown으로 전송했습니다.')
}

function createContextMenuSafe(): void {
  if (typeof chrome === 'undefined' || !chrome.contextMenus?.create) {
    browser.contextMenus.create({
      id: contextMenuId,
      title: 'Download with Gomdown',
      visible: true,
      contexts: ['all'],
    })
    return
  }
  chrome.contextMenus.create(
    {
      id: contextMenuId,
      title: 'Download with Gomdown',
      contexts: ['all'],
    },
    () => {
      void chrome.runtime.lastError
    }
  )
}

function setupContextMenuClickListener(): void {
  if (contextMenuHooked) return
  if (typeof chrome !== 'undefined' && chrome.contextMenus?.onClicked) {
    chrome.contextMenus.onClicked.addListener((info, tab) => {
      void handleContextMenuClick(info, tab)
    })
  } else {
    browser.contextMenus.onClicked.addListener((info: any, tab: any) => {
      void handleContextMenuClick(info, tab)
    })
  }
  contextMenuHooked = true
}

async function createMenuItemInternal(): Promise<void> {
  const settings = await getSettings()
  if (!settings.extensionStatus || !settings.showContextOption) {
    await browser.contextMenus.removeAll().catch(() => null)
    return
  }

  await browser.contextMenus.removeAll().catch(() => null)
  createContextMenuSafe()
}

function createMenuItem(): Promise<void> {
  if (contextMenuUpdateInFlight) return contextMenuUpdateInFlight
  contextMenuUpdateInFlight = createMenuItemInternal().finally(() => {
    contextMenuUpdateInFlight = null
  })
  return contextMenuUpdateInFlight
}

browser.runtime.onMessage.addListener((message: any, sender: any) => {
  if (message?.type === 'capture-link-download') {
    const url = String(message?.url || '').trim()
    if (!url) return Promise.resolve({ ok: false, error: 'url is empty' })

    const senderTabId = Number(sender?.tab?.id)
    return transferUrlToGdown(url, String(message?.referer || '')).then((result) => {
      if (result.ok) {
        rememberCapturedUrl(url)
        rememberCapturedTab(senderTabId)
      }
      return result
    })
  }

  if (message?.type === 'media:list') {
    return listMediaCandidates().then((items) => ({ ok: true, items }))
  }

  if (message?.type === 'media:clear') {
    return clearMediaCandidates().then(() => ({ ok: true }))
  }

  if (message?.type === 'media:enqueue') {
    const url = String(message?.url || '').trim()
    if (!url) return Promise.resolve({ ok: false, error: 'url is empty' })
    return transferUrlToGdown(url, String(message?.referer || '')).then((result) => result)
  }

  if (message?.type === 'page:enqueue-ytdlp') {
    return browser.tabs
      .query({ active: true, currentWindow: true })
      .then(async (tabs) => {
        const tab = tabs[0]
        const url = String(tab?.url || '').trim()
        if (!url) return { ok: false, error: 'active tab url is empty' }
        return transferUrlToGdown(url, url, 'yt-dlp')
      })
  }

  return undefined
})

browser.runtime.onInstalled.addListener(() => {
  console.log('[gomdown-helper] onInstalled')
  setupWebRequestInterceptor()
  setupDownloadCancelHook()
  setupContextMenuClickListener()
  void createMenuItem()
  void applyShelfVisibility()
})

browser.runtime.onStartup.addListener(() => {
  console.log('[gomdown-helper] onStartup')
  setupWebRequestInterceptor()
  setupDownloadCancelHook()
  setupContextMenuClickListener()
  void createMenuItem()
  void applyShelfVisibility()
})

browser.storage.onChanged.addListener((changes, areaName) => {
  if (areaName !== 'sync') return
  if (changes.hideChromeBar || changes.useNativeHost || changes.extensionStatus) {
    void applyShelfVisibility()
    void createMenuItem()
  }
  if (changes.showContextOption) {
    void createMenuItem()
  }
})

setupWebRequestInterceptor()
setupDownloadCancelHook()
setupContextMenuClickListener()
void createMenuItem()
void applyShelfVisibility()
console.log('[gomdown-helper] service worker initialized')
