<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { getCurrentWindow } from '@tauri-apps/api/window'
import type { UnlistenFn } from '@tauri-apps/api/event'
import { open as openDialog } from '@tauri-apps/plugin-dialog'
import { getCurrent as getCurrentDeepLink, onOpenUrl } from '@tauri-apps/plugin-deep-link'
import {
  addAria2Torrent,
  addAria2Uri,
  changeAria2GlobalOption,
  detectAria2Binary,
  getEngineStatus,
  getAria2TaskDetail,
  listAria2Tasks,
  loadTorrentFile,
  pauseAllAria2,
  pauseAria2Task,
  openPathInFileManager,
  focusMainWindow,
  takeExternalAddRequests,
  removeAria2Task,
  removeAria2TaskRecord,
  resumeAllAria2,
  resumeAria2Task,
  startEngine,
  stopEngine,
  type Aria2Task,
  type Aria2TaskDetail,
  type Aria2TaskSnapshot,
  type EngineStatus,
  type ExternalAddRequest,
} from './lib/engineApi'

type TaskFilter = 'all' | 'active' | 'waiting' | 'stopped' | 'completed'
type AddTab = 'url' | 'torrent'
type AppPage = 'downloads' | 'settings'
type SettingsTab = 'basic' | 'advanced' | 'lab'
type TaskInfoTab = 'general' | 'activity' | 'trackers' | 'peers' | 'files'
type PersistedSettings = {
  binaryPath?: string
  rpcPort?: number
  rpcSecret?: string
  downloadDir?: string
  split?: number
  maxConcurrentDownloads?: number
  autoRefresh?: boolean
  theme?: 'auto' | 'light' | 'dark'
  hideWindowOnStartup?: boolean
  traySpeed?: boolean
  showDockProgress?: boolean
  language?: string
  runOnLogin?: boolean
  rememberWindow?: boolean
  autoResume?: boolean
  uploadLimit?: number
  downloadLimit?: number
  magnetAsTorrent?: boolean
  autoDownloadTorrentMeta?: boolean
  btForceEncryption?: boolean
  keepSeeding?: boolean
  seedRatio?: number
  autoCheckUpdate?: boolean
  proxyEnabled?: boolean
  proxyServer?: string
  proxyBypass?: string
  trackerSources?: string[]
  trackerList?: string
  autoSyncTracker?: boolean
  lastSyncTrackerTime?: number
  lastCheckUpdateTime?: number
  btListenPort?: number
  dhtListenPort?: number
  enableUpnp?: boolean
  protocolMagnet?: boolean
  protocolThunder?: boolean
  userAgent?: string
  skipTlsVerify?: boolean
}

const SETTINGS_STORAGE_KEY = 'gdown.settings.v1'

const binaryPath = ref('aria2c')
const rpcPort = ref(6800)
const rpcSecret = ref('')
const activeRpcPort = ref(6800)
const activeRpcSecret = ref('')
const downloadDir = ref('')
const split = ref(8)
const maxConcurrentDownloads = ref(5)

const loadingEngine = ref(false)
const loadingTasks = ref(false)
const loadingAddTask = ref(false)
const loadingTaskAction = ref(false)
const errorMessage = ref('')
const successMessage = ref('')
const toastVisible = ref(false)
const toastMessage = ref('')
const toastType = ref<'success' | 'error'>('success')
const autoRefresh = ref(true)
const filter = ref<TaskFilter>('active')
const page = ref<AppPage>('downloads')
const settingsTab = ref<SettingsTab>('basic')

const settingTheme = ref<'auto' | 'light' | 'dark'>('dark')
const settingHideWindowOnStartup = ref(false)
const settingTraySpeed = ref(true)
const settingShowDockProgress = ref(true)
const settingLanguage = ref('한국어')
const settingRunOnLogin = ref(false)
const settingRememberWindow = ref(true)
const settingAutoResume = ref(true)
const settingUploadLimit = ref(256)
const settingDownloadLimit = ref(0)
const settingMagnetAsTorrent = ref(true)
const settingAutoDownloadTorrentMeta = ref(true)
const settingBtForceEncryption = ref(false)
const settingKeepSeeding = ref(true)
const settingSeedRatio = ref(1)
const settingAutoCheckUpdate = ref(true)
const settingProxyEnabled = ref(false)
const settingProxyServer = ref('')
const settingProxyBypass = ref('')
const settingTrackerSources = ref<string[]>([
  'https://cdn.jsdelivr.net/gh/ngosang/trackerslist/trackers_best_ip.txt',
  'https://cdn.jsdelivr.net/gh/ngosang/trackerslist/trackers_best.txt',
])
const settingTrackerSourceInput = ref(settingTrackerSources.value.join('\n'))
const settingTrackerList = ref('')
const settingAutoSyncTracker = ref(true)
const settingLastSyncTrackerTime = ref(0)
const settingLastCheckUpdateTime = ref(0)
const settingBtListenPort = ref(21301)
const settingDhtListenPort = ref(26701)
const settingEnableUpnp = ref(true)
const settingProtocolMagnet = ref(true)
const settingProtocolThunder = ref(false)
const settingUserAgent = ref('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122 Safari/537.36')
const settingSkipTlsVerify = ref(true)
const trackerSyncing = ref(false)
const rpcTestLoading = ref(false)
const rpcTestStatus = ref<'idle' | 'ok' | 'fail'>('idle')
const rpcTestMessage = ref('')

const showAddModal = ref(false)
const addTab = ref<AddTab>('url')
const addUrl = ref('')
const addOut = ref('')
const addSplit = ref(64)
const addShowAdvanced = ref(false)
const addUserAgent = ref('')
const addAuthorization = ref('')
const addReferer = ref('')
const addCookie = ref('')
const addProxy = ref('')
const addNavigateToDownloading = ref(true)
const torrentBase64 = ref('')
const torrentFileName = ref('')
const torrentFileExt = ref('')
const torrentFileSize = ref(0)
const appDropActive = ref(false)
const modalDropActive = ref(false)
const torrentFileInput = ref<HTMLInputElement | null>(null)
const taskInfoVisible = ref(false)
const taskInfoLoading = ref(false)
const taskInfoTab = ref<TaskInfoTab>('general')
const taskInfoTask = ref<Aria2Task | null>(null)
const taskInfoDetail = ref<Aria2TaskDetail | null>(null)

const status = ref<EngineStatus>({
  running: false,
  pid: null,
  binaryPath: null,
  args: [],
  startedAt: null,
})

const tasks = ref<Aria2TaskSnapshot>({
  active: [],
  waiting: [],
  stopped: [],
})

let refreshTimer: number | null = null
let unlistenDragDrop: UnlistenFn | null = null
let unlistenDeepLink: UnlistenFn | null = null
let toastTimer: number | null = null
let externalAddPollTimer: number | null = null

async function focusAppWindowForDeepLink() {
  try {
    await focusMainWindow()
  } catch {
    const appWindow = getCurrentWindow()
    try {
      await appWindow.show()
      await appWindow.unminimize()
      await appWindow.setFocus()
    } catch {
      // ignore focus fallback failures
    }
  }
}

type ExternalAddPayload = ExternalAddRequest

function parseExternalAddDeepLink(rawUrl: string): ExternalAddPayload | null {
  try {
    const parsed = new URL(rawUrl)
    if (parsed.protocol !== 'gdown:') return null
    if (parsed.hostname !== 'add') return null
    const url = parsed.searchParams.get('url')?.trim() || ''
    if (!url) return null

    const splitRaw = parsed.searchParams.get('split') ?? ''
    const split = Number.parseInt(splitRaw, 10)

    return {
      url,
      out: parsed.searchParams.get('out')?.trim() || undefined,
      dir: parsed.searchParams.get('dir')?.trim() || undefined,
      referer: parsed.searchParams.get('referer')?.trim() || undefined,
      userAgent: parsed.searchParams.get('userAgent')?.trim() || undefined,
      authorization: parsed.searchParams.get('authorization')?.trim() || undefined,
      cookie: parsed.searchParams.get('cookie')?.trim() || undefined,
      proxy: parsed.searchParams.get('proxy')?.trim() || undefined,
      split: Number.isFinite(split) && split > 0 ? split : undefined,
    }
  } catch {
    return null
  }
}

function applyExternalAddPayload(payload: ExternalAddPayload) {
  showAddModal.value = true
  addTab.value = 'url'
  const current = addUrl.value.trim()
  if (!current) {
    addUrl.value = payload.url
  } else if (!current.split('\n').map((line) => line.trim()).includes(payload.url)) {
    addUrl.value = `${addUrl.value}\n${payload.url}`
  }

  if (payload.out) addOut.value = payload.out
  if (payload.dir) downloadDir.value = payload.dir
  if (payload.referer) addReferer.value = payload.referer
  if (payload.userAgent) addUserAgent.value = payload.userAgent
  if (payload.authorization) addAuthorization.value = payload.authorization
  if (payload.cookie) addCookie.value = payload.cookie
  if (payload.proxy) addProxy.value = payload.proxy
  if (payload.split) addSplit.value = payload.split

  if (payload.referer || payload.userAgent || payload.authorization || payload.cookie || payload.proxy) {
    addShowAdvanced.value = true
  }
  page.value = 'downloads'
  filter.value = 'active'
  pushSuccess('외부 다운로드 요청이 추가 창에 채워졌습니다. 확인을 눌러 시작하세요.')
}

async function pollExternalAddQueue() {
  try {
    const requests = await takeExternalAddRequests()
    if (!Array.isArray(requests) || requests.length === 0) return
    await focusAppWindowForDeepLink()
    requests.forEach((payload) => {
      if (payload?.url?.trim()) {
        applyExternalAddPayload(payload)
      }
    })
  } catch {
    // ignore queue poll failures
  }
}

async function handleDeepLinkUrls(urls: string[] | null | undefined) {
  if (!Array.isArray(urls)) return
  const addPayloads: ExternalAddPayload[] = []
  let shouldFocus = false

  urls.forEach((url) => {
    if (typeof url !== 'string') return
    if (url.startsWith('gdown://focus')) {
      shouldFocus = true
      return
    }
    const payload = parseExternalAddDeepLink(url)
    if (payload) {
      addPayloads.push(payload)
      shouldFocus = true
    }
  })

  if (!shouldFocus) return
  await focusAppWindowForDeepLink()
  addPayloads.forEach((payload) => applyExternalAddPayload(payload))
}

function isCompletedTask(task: Aria2Task): boolean {
  if (task.status === 'complete') return true
  const total = Number(task.totalLength)
  const done = Number(task.completedLength)
  return Number.isFinite(total) && Number.isFinite(done) && total > 0 && done >= total
}

const completedTasks = computed(() => tasks.value.stopped.filter((task) => isCompletedTask(task)))
const stoppedTasks = computed(() => tasks.value.stopped.filter((task) => !isCompletedTask(task)))

const filteredTasks = computed(() => {
  if (filter.value === 'active') return tasks.value.active
  if (filter.value === 'waiting') return tasks.value.waiting
  if (filter.value === 'stopped') return stoppedTasks.value
  if (filter.value === 'completed') return completedTasks.value
  return [...tasks.value.active, ...tasks.value.waiting, ...stoppedTasks.value, ...completedTasks.value]
})
const filteredTaskCount = computed(() => filteredTasks.value.length)
const taskInfoLiveTask = computed(() => {
  const gid = taskInfoTask.value?.gid
  if (!gid) return null
  return [...tasks.value.active, ...tasks.value.waiting, ...tasks.value.stopped].find((task) => task.gid === gid) ?? null
})
const taskInfoActionTask = computed(() => taskInfoLiveTask.value ?? taskInfoTask.value)
const taskInfoProgress = computed(() => {
  if (!taskInfoLiveTask.value) return 0
  return progress(taskInfoLiveTask.value)
})
const taskInfoRatio = computed(() => {
  const completed = Number(taskInfoDetail.value?.completedLength ?? '0')
  const uploaded = Number(taskInfoDetail.value?.uploadLength ?? '0')
  if (!Number.isFinite(completed) || completed <= 0 || !Number.isFinite(uploaded)) return '0'
  return (uploaded / completed).toFixed(2)
})
const taskInfoSelectedFilesText = computed(() => {
  const files = taskInfoDetail.value?.files ?? []
  const selected = files.filter((file) => file.selected === 'true' || file.selected === '1')
  const selectedCount = selected.length
  const size = selected.reduce((sum, file) => sum + (Number(file.length) || 0), 0)
  if (selectedCount === 0) return '파일 0개 선택됨'
  return `파일 ${selectedCount}개 선택됨, 총 ${formatBytesNumber(size)}`
})
const rpcEndpointText = computed(() => `http://127.0.0.1:${sanitizePort(rpcPort.value, 6800)}/jsonrpc`)
const rpcTokenText = computed(() => {
  const secret = rpcSecret.value.trim()
  return secret ? `token:${secret}` : ''
})
const lastSyncTrackerTimeText = computed(() => {
  if (settingLastSyncTrackerTime.value <= 0) return '동기화 이력 없음'
  return new Date(settingLastSyncTrackerTime.value).toLocaleString()
})
const lastCheckUpdateTimeText = computed(() => {
  if (settingLastCheckUpdateTime.value <= 0) return '업데이트 확인 이력 없음'
  return new Date(settingLastCheckUpdateTime.value).toLocaleString()
})

function showToast(type: 'success' | 'error', message: string) {
  toastType.value = type
  toastMessage.value = message
  toastVisible.value = true

  if (toastTimer !== null) {
    window.clearTimeout(toastTimer)
  }
  toastTimer = window.setTimeout(() => {
    toastVisible.value = false
    toastTimer = null
  }, 2200)
}

function loadSettingsFromStorage() {
  try {
    const raw = localStorage.getItem(SETTINGS_STORAGE_KEY)
    if (!raw) return
    const parsed = JSON.parse(raw) as PersistedSettings
    if (typeof parsed.binaryPath === 'string') binaryPath.value = parsed.binaryPath
    if (typeof parsed.rpcPort === 'number' && Number.isFinite(parsed.rpcPort)) rpcPort.value = parsed.rpcPort
    if (typeof parsed.rpcSecret === 'string') rpcSecret.value = parsed.rpcSecret
    if (typeof parsed.downloadDir === 'string') downloadDir.value = parsed.downloadDir
    if (typeof parsed.split === 'number' && Number.isFinite(parsed.split)) split.value = parsed.split
    if (typeof parsed.maxConcurrentDownloads === 'number' && Number.isFinite(parsed.maxConcurrentDownloads)) {
      maxConcurrentDownloads.value = parsed.maxConcurrentDownloads
    }
    if (typeof parsed.autoRefresh === 'boolean') autoRefresh.value = parsed.autoRefresh
    if (parsed.theme === 'auto' || parsed.theme === 'light' || parsed.theme === 'dark') {
      settingTheme.value = parsed.theme
    }
    if (typeof parsed.hideWindowOnStartup === 'boolean') settingHideWindowOnStartup.value = parsed.hideWindowOnStartup
    if (typeof parsed.traySpeed === 'boolean') settingTraySpeed.value = parsed.traySpeed
    if (typeof parsed.showDockProgress === 'boolean') settingShowDockProgress.value = parsed.showDockProgress
    if (typeof parsed.language === 'string') settingLanguage.value = parsed.language
    if (typeof parsed.runOnLogin === 'boolean') settingRunOnLogin.value = parsed.runOnLogin
    if (typeof parsed.rememberWindow === 'boolean') settingRememberWindow.value = parsed.rememberWindow
    if (typeof parsed.autoResume === 'boolean') settingAutoResume.value = parsed.autoResume
    if (typeof parsed.uploadLimit === 'number') settingUploadLimit.value = parsed.uploadLimit
    if (typeof parsed.downloadLimit === 'number') settingDownloadLimit.value = parsed.downloadLimit
    if (typeof parsed.magnetAsTorrent === 'boolean') settingMagnetAsTorrent.value = parsed.magnetAsTorrent
    if (typeof parsed.autoDownloadTorrentMeta === 'boolean') {
      settingAutoDownloadTorrentMeta.value = parsed.autoDownloadTorrentMeta
    }
    if (typeof parsed.btForceEncryption === 'boolean') settingBtForceEncryption.value = parsed.btForceEncryption
    if (typeof parsed.keepSeeding === 'boolean') settingKeepSeeding.value = parsed.keepSeeding
    if (typeof parsed.seedRatio === 'number') settingSeedRatio.value = parsed.seedRatio
    if (typeof parsed.autoCheckUpdate === 'boolean') settingAutoCheckUpdate.value = parsed.autoCheckUpdate
    if (typeof parsed.proxyEnabled === 'boolean') settingProxyEnabled.value = parsed.proxyEnabled
    if (typeof parsed.proxyServer === 'string') settingProxyServer.value = parsed.proxyServer
    if (typeof parsed.proxyBypass === 'string') settingProxyBypass.value = parsed.proxyBypass
    if (Array.isArray(parsed.trackerSources)) {
      settingTrackerSources.value = parsed.trackerSources.filter((v): v is string => typeof v === 'string')
      settingTrackerSourceInput.value = settingTrackerSources.value.join('\n')
    }
    if (typeof parsed.trackerList === 'string') settingTrackerList.value = parsed.trackerList
    if (typeof parsed.autoSyncTracker === 'boolean') settingAutoSyncTracker.value = parsed.autoSyncTracker
    if (typeof parsed.lastSyncTrackerTime === 'number') settingLastSyncTrackerTime.value = parsed.lastSyncTrackerTime
    if (typeof parsed.lastCheckUpdateTime === 'number') settingLastCheckUpdateTime.value = parsed.lastCheckUpdateTime
    if (typeof parsed.btListenPort === 'number') settingBtListenPort.value = parsed.btListenPort
    if (typeof parsed.dhtListenPort === 'number') settingDhtListenPort.value = parsed.dhtListenPort
    if (typeof parsed.enableUpnp === 'boolean') settingEnableUpnp.value = parsed.enableUpnp
    if (typeof parsed.protocolMagnet === 'boolean') settingProtocolMagnet.value = parsed.protocolMagnet
    if (typeof parsed.protocolThunder === 'boolean') settingProtocolThunder.value = parsed.protocolThunder
    if (typeof parsed.userAgent === 'string') settingUserAgent.value = parsed.userAgent
    if (typeof parsed.skipTlsVerify === 'boolean') settingSkipTlsVerify.value = parsed.skipTlsVerify
  } catch {
    // ignore malformed settings
  }
}

function saveSettingsToStorage() {
  const payload: PersistedSettings = {
    binaryPath: binaryPath.value,
    rpcPort: rpcPort.value,
    rpcSecret: rpcSecret.value,
    downloadDir: downloadDir.value,
    split: split.value,
    maxConcurrentDownloads: maxConcurrentDownloads.value,
    autoRefresh: autoRefresh.value,
    theme: settingTheme.value,
    hideWindowOnStartup: settingHideWindowOnStartup.value,
    traySpeed: settingTraySpeed.value,
    showDockProgress: settingShowDockProgress.value,
    language: settingLanguage.value,
    runOnLogin: settingRunOnLogin.value,
    rememberWindow: settingRememberWindow.value,
    autoResume: settingAutoResume.value,
    uploadLimit: settingUploadLimit.value,
    downloadLimit: settingDownloadLimit.value,
    magnetAsTorrent: settingMagnetAsTorrent.value,
    autoDownloadTorrentMeta: settingAutoDownloadTorrentMeta.value,
    btForceEncryption: settingBtForceEncryption.value,
    keepSeeding: settingKeepSeeding.value,
    seedRatio: settingSeedRatio.value,
    autoCheckUpdate: settingAutoCheckUpdate.value,
    proxyEnabled: settingProxyEnabled.value,
    proxyServer: settingProxyServer.value,
    proxyBypass: settingProxyBypass.value,
    trackerSources: settingTrackerSources.value,
    trackerList: settingTrackerList.value,
    autoSyncTracker: settingAutoSyncTracker.value,
    lastSyncTrackerTime: settingLastSyncTrackerTime.value,
    lastCheckUpdateTime: settingLastCheckUpdateTime.value,
    btListenPort: settingBtListenPort.value,
    dhtListenPort: settingDhtListenPort.value,
    enableUpnp: settingEnableUpnp.value,
    protocolMagnet: settingProtocolMagnet.value,
    protocolThunder: settingProtocolThunder.value,
    userAgent: settingUserAgent.value,
    skipTlsVerify: settingSkipTlsVerify.value,
  }
  localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(payload))
}

function pushError(message: string) {
  successMessage.value = ''
  errorMessage.value = message
  showToast('error', message)
}

function pushSuccess(message: string) {
  errorMessage.value = ''
  successMessage.value = message
  showToast('success', message)
}

function rpcConfig() {
  return {
    rpcListenPort: activeRpcPort.value,
    rpcSecret: activeRpcSecret.value.trim() || undefined,
  }
}

function formatBytes(value: string): string {
  const n = Number(value)
  if (!Number.isFinite(n)) return '-'
  return formatBytesNumber(n)
}

function formatBytesNumber(n: number): string {
  if (!Number.isFinite(n)) return '-'
  if (n < 1024) return `${n} B`
  const kb = n / 1024
  if (kb < 1024) return `${kb.toFixed(1)} KB`
  const mb = kb / 1024
  if (mb < 1024) return `${mb.toFixed(1)} MB`
  const gb = mb / 1024
  return `${gb.toFixed(2)} GB`
}

function formatSpeed(value: string): string {
  const n = Number(value)
  if (!Number.isFinite(n) || n <= 0) return '0 B/s'
  if (n < 1024) return `${n} B/s`
  const kb = n / 1024
  if (kb < 1024) return `${kb.toFixed(1)} KB/s`
  const mb = kb / 1024
  return `${mb.toFixed(1)} MB/s`
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => {
    window.setTimeout(resolve, ms)
  })
}

function formatUnixSeconds(value: string): string {
  const n = Number(value)
  if (!Number.isFinite(n) || n <= 0) return '-'
  return new Date(n * 1000).toLocaleString()
}

function fileExtFromPath(path: string): string {
  const idx = path.lastIndexOf('.')
  if (idx < 0) return '-'
  return path.slice(idx + 1).toLowerCase() || '-'
}

function fileNameFromPath(path: string): string {
  const normalized = path.replace(/\\/g, '/')
  const idx = normalized.lastIndexOf('/')
  if (idx < 0) return normalized
  return normalized.slice(idx + 1)
}

function progress(task: Aria2Task): number {
  const total = Number(task.totalLength)
  const done = Number(task.completedLength)
  if (!Number.isFinite(total) || total <= 0 || !Number.isFinite(done)) return 0
  return Math.min(100, Math.max(0, (done / total) * 100))
}

async function refreshEngineStatus() {
  try {
    status.value = await getEngineStatus()
  } catch (error) {
    pushError(String(error))
  }
}

async function refreshTasks(silent = false) {
  if (!status.value.running) {
    tasks.value = { active: [], waiting: [], stopped: [] }
    return
  }

  if (!silent) loadingTasks.value = true
  try {
    tasks.value = await listAria2Tasks(rpcConfig())
  } catch (error) {
    pushError(String(error))
  } finally {
    if (!silent) loadingTasks.value = false
  }
}

async function refreshTaskInfo(silent = false) {
  if (!taskInfoVisible.value || !taskInfoTask.value) return
  if (!status.value.running) return
  if (taskInfoLoading.value && silent) return

  if (!silent) taskInfoLoading.value = true
  try {
    taskInfoDetail.value = await getAria2TaskDetail({
      rpc: rpcConfig(),
      gid: taskInfoTask.value.gid,
    })
  } catch (error) {
    if (!silent) {
      pushError(String(error))
    }
  } finally {
    if (!silent) taskInfoLoading.value = false
  }
}

function updateRefreshTimer() {
  if (refreshTimer !== null) {
    window.clearInterval(refreshTimer)
    refreshTimer = null
  }
  if (!autoRefresh.value) return

  refreshTimer = window.setInterval(() => {
    void refreshEngineStatus()
    void refreshTasks(true)
    void refreshTaskInfo(true)
  }, 3000)
}

async function autoStartEngine(silent = true) {
  loadingEngine.value = true
  try {
    let resolvedBinaryPath = binaryPath.value.trim() || undefined
    const probe = await detectAria2Binary(resolvedBinaryPath).catch(() => null)
    if (probe?.found && probe.binaryPath) {
      binaryPath.value = probe.binaryPath
      resolvedBinaryPath = probe.binaryPath
    }

    status.value = await startEngine({
      binaryPath: resolvedBinaryPath,
      rpcListenPort: rpcPort.value,
      rpcSecret: rpcSecret.value.trim() || undefined,
      downloadDir: downloadDir.value.trim() || undefined,
      maxConcurrentDownloads: maxConcurrentDownloads.value,
      split: split.value,
    })
    activeRpcPort.value = rpcPort.value
    activeRpcSecret.value = rpcSecret.value.trim()
    saveSettingsToStorage()
    if (!silent) pushSuccess('aria2 engine started.')
    await refreshTasks()
    return true
  } catch (error) {
    pushError(String(error))
    return false
  } finally {
    loadingEngine.value = false
  }
}

async function ensureEngineRunning() {
  if (status.value.running) return true
  await refreshEngineStatus()
  if (status.value.running) return true
  return autoStartEngine(false)
}

async function onPauseAllTasks() {
  const ready = await ensureEngineRunning()
  if (!ready) return
  loadingTaskAction.value = true
  try {
    await pauseAllAria2(rpcConfig())
    pushSuccess('모든 작업을 일시정지했습니다.')
    await refreshTasks()
  } catch (error) {
    pushError(String(error))
  } finally {
    loadingTaskAction.value = false
  }
}

async function onResumeAllTasks() {
  const ready = await ensureEngineRunning()
  if (!ready) return
  loadingTaskAction.value = true
  try {
    await resumeAllAria2(rpcConfig())
    pushSuccess('모든 작업을 재개했습니다.')
    await refreshTasks()
  } catch (error) {
    pushError(String(error))
  } finally {
    loadingTaskAction.value = false
  }
}

async function onTaskAction(task: Aria2Task, action: 'pause' | 'resume' | 'remove' | 'purge') {
  const ready = await ensureEngineRunning()
  if (!ready) return
  loadingTaskAction.value = true
  try {
    const payload = { rpc: rpcConfig(), gid: task.gid }
    if (action === 'pause') {
      await pauseAria2Task(payload)
    } else if (action === 'resume') {
      await resumeAria2Task(payload)
    } else if (action === 'remove') {
      await removeAria2Task(payload)
    } else {
      await removeAria2TaskRecord(payload)
    }
    await refreshTasks()
  } catch (error) {
    pushError(String(error))
  } finally {
    loadingTaskAction.value = false
  }
}

async function onRemoveFilteredTasks() {
  const ready = await ensureEngineRunning()
  if (!ready) return
  const targetTasks = [...filteredTasks.value]
  if (targetTasks.length === 0) return

  loadingTaskAction.value = true
  try {
    for (const task of targetTasks) {
      const payload = { rpc: rpcConfig(), gid: task.gid }
      if (task.status === 'stopped') {
        await removeAria2TaskRecord(payload)
      } else {
        await removeAria2Task(payload)
      }
    }
    if (filter.value === 'stopped') {
      pushSuccess(`${targetTasks.length}개 기록을 정리했습니다.`)
    } else {
      pushSuccess(`${targetTasks.length}개 작업을 삭제했습니다.`)
    }
    await refreshTasks()
  } catch (error) {
    pushError(String(error))
  } finally {
    loadingTaskAction.value = false
  }
}

function taskPrimaryAction(task: Aria2Task): 'pause' | 'resume' {
  return task.status === 'active' ? 'pause' : 'resume'
}

function taskPrimaryIcon(task: Aria2Task): string {
  return task.status === 'active' ? '□' : '▶'
}

function taskPrimaryTitle(task: Aria2Task): string {
  return task.status === 'active' ? '정지' : '재개'
}

async function onOpenTaskFolder(task: Aria2Task) {
  const dir = task.dir?.trim()
  if (!dir) {
    pushError('작업 폴더 경로가 없습니다.')
    return
  }
  try {
    await openPathInFileManager(dir)
  } catch (error) {
    pushError(String(error))
  }
}

async function onCopyTaskLink(task: Aria2Task) {
  const text = task.uri?.trim() || task.gid
  try {
    await navigator.clipboard.writeText(text)
    pushSuccess('작업 링크를 클립보드에 복사했습니다.')
  } catch (error) {
    pushError(`클립보드 복사 실패: ${error}`)
  }
}

async function copyRpcEndpoint() {
  try {
    await navigator.clipboard.writeText(rpcEndpointText.value)
    pushSuccess('RPC URL을 복사했습니다.')
  } catch (error) {
    pushError(`RPC URL 복사 실패: ${error}`)
  }
}

async function copyRpcToken() {
  if (!rpcTokenText.value) {
    pushError('RPC 비밀이 비어 있습니다.')
    return
  }
  try {
    await navigator.clipboard.writeText(rpcTokenText.value)
    pushSuccess('RPC 토큰을 복사했습니다.')
  } catch (error) {
    pushError(`RPC 토큰 복사 실패: ${error}`)
  }
}

async function testRpcConnection() {
  rpcTestLoading.value = true
  rpcTestStatus.value = 'idle'
  rpcTestMessage.value = ''
  try {
    await listAria2Tasks({
      rpcListenPort: sanitizePort(rpcPort.value, 6800),
      rpcSecret: rpcSecret.value.trim() || undefined,
    })
    rpcTestStatus.value = 'ok'
    rpcTestMessage.value = '연결 성공'
    pushSuccess('RPC 연결 성공')
  } catch (error) {
    rpcTestStatus.value = 'fail'
    rpcTestMessage.value = String(error)
    pushError(`RPC 연결 실패: ${error}`)
  } finally {
    rpcTestLoading.value = false
  }
}

function onShowTaskInfo(task: Aria2Task) {
  taskInfoTask.value = task
  taskInfoTab.value = 'general'
  taskInfoVisible.value = true
  void refreshTaskInfo()
}

function closeTaskInfo() {
  taskInfoVisible.value = false
  taskInfoTask.value = null
  taskInfoDetail.value = null
}

function statusLabel(status: string | undefined): string {
  if (!status) return '-'
  const upper = status.toUpperCase()
  if (upper === 'ACTIVE') return 'DOWNLOADING'
  if (upper === 'COMPLETE') return 'SEEDING'
  return upper
}

function parsePeerClient(peerId: string): string {
  const value = peerId.trim()
  if (!value) return '-'

  const matched = /^-([A-Za-z0-9]{2})([0-9]{4})-/.exec(value)
  if (!matched) {
    return value.slice(0, 12)
  }

  const prefix = matched[1] ?? ''
  const versionRaw = (matched[2] ?? '').padEnd(4, '0')
  const v0 = versionRaw.charAt(0)
  const v1 = versionRaw.charAt(1)
  const v2 = versionRaw.charAt(2)
  const v3 = versionRaw.charAt(3)
  const version = `${v0}.${v1}.${v2}${v3 ? `.${v3}` : ''}`
  const map: Record<string, string> = {
    AG: 'Ares',
    AR: 'Arctic',
    AT: 'Artemis',
    AV: 'Avicora',
    AX: 'BitPump',
    AZ: 'Azureus',
    BC: 'BitComet',
    BF: 'Bitflu',
    BR: 'BitRocket',
    BT: 'BT',
    DE: 'Deluge',
    HL: 'Halite',
    KT: 'KTorrent',
    LT: 'libtorrent',
    MT: 'MoonlightTorrent',
    qB: 'qBittorrent',
    TR: 'Transmission',
    UT: 'uTorrent',
    UM: 'uTorrent Mac',
    XL: 'Xunlei',
  }
  const name = map[prefix] ?? prefix
  return `${name} ${version}`
}

function countBitsInHex(hex: string): number {
  let count = 0
  for (const ch of hex) {
    const nibble = Number.parseInt(ch, 16)
    if (!Number.isFinite(nibble) || nibble < 0) continue
    count += nibble.toString(2).split('').filter((bit) => bit === '1').length
  }
  return count
}

function peerPercent(peer: { bitfield: string }, detail: Aria2TaskDetail | null): string {
  const bitfield = peer.bitfield?.trim()
  if (!bitfield) return '-'
  const havePieces = countBitsInHex(bitfield)
  if (havePieces <= 0) return '0'

  const pieces = Number(detail?.numPieces ?? '0')
  if (Number.isFinite(pieces) && pieces > 0) {
    return `${Math.min(100, Math.round((havePieces / pieces) * 100))}`
  }

  const totalBits = bitfield.length * 4
  if (totalBits <= 0) return '-'
  return `${Math.min(100, Math.round((havePieces / totalBits) * 100))}`
}

async function onTaskInfoPrimaryAction() {
  const task = taskInfoActionTask.value
  if (!task) return
  await onTaskAction(task, taskPrimaryAction(task))
}

async function onTaskInfoRemove() {
  const task = taskInfoActionTask.value
  if (!task) return
  await onTaskAction(task, task.status === 'stopped' ? 'purge' : 'remove')
}

async function inspectAddedTask(gid: string) {
  try {
    await sleep(900)
    const detail = await getAria2TaskDetail({
      rpc: rpcConfig(),
      gid,
    })
    if (detail.status === 'error') {
      page.value = 'downloads'
      filter.value = 'stopped'
      const message = detail.errorMessage || `errorCode=${detail.errorCode || '-'}`
      pushError(`작업 추가 실패: ${message}`)
      return
    }
    if (detail.status === 'removed') {
      page.value = 'downloads'
      filter.value = 'stopped'
      pushError('작업이 즉시 제거되었습니다. 링크/권한/RPC 설정을 확인하세요.')
    }
  } catch {
    // ignore follow-up inspect failures
  }
}

function openAddModal() {
  showAddModal.value = true
  addTab.value = 'url'
}

function openDownloadsPage() {
  page.value = 'downloads'
}

function openSettingsPage() {
  page.value = 'settings'
}

function sanitizePort(value: number, fallback: number): number {
  if (!Number.isFinite(value)) return fallback
  const rounded = Math.round(value)
  return Math.max(1, Math.min(65535, rounded))
}

function normalizeLineItems(value: string): string[] {
  return value
    .split('\n')
    .map((line) => line.trim())
    .filter((line) => line.length > 0)
}

function trackerTextToAria2Value(value: string): string {
  const lines = normalizeLineItems(value).filter((line) => !line.startsWith('#'))
  return lines.join(',')
}

function toLimitBytesPerSecond(kbPerSecond: number): string {
  if (!Number.isFinite(kbPerSecond) || kbPerSecond <= 0) return '0'
  return String(Math.floor(kbPerSecond * 1024))
}

function boolToAria(value: boolean): string {
  return value ? 'true' : 'false'
}

function randomPort(): number {
  return Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025
}

function randomSecret(length = 24): string {
  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789'
  let result = ''
  for (let i = 0; i < length; i += 1) {
    result += chars[Math.floor(Math.random() * chars.length)] ?? 'a'
  }
  return result
}

async function syncTrackerFromSources() {
  const sources = normalizeLineItems(settingTrackerSourceInput.value)
  settingTrackerSources.value = sources
  if (sources.length === 0) {
    pushError('트래커 소스 URL을 하나 이상 입력하세요.')
    return
  }

  trackerSyncing.value = true
  try {
    const responses = await Promise.all(sources.map(async (source) => fetch(source)))
    const texts = await Promise.all(
      responses.map(async (response, idx) => {
        if (!response.ok) {
          throw new Error(`트래커 소스 응답 실패(${response.status}): ${sources[idx]}`)
        }
        return response.text()
      }),
    )

    const merged = new Set<string>()
    texts.forEach((text) => {
      text
        .split('\n')
        .map((line) => line.trim())
        .filter((line) => line.length > 0 && !line.startsWith('#'))
        .forEach((line) => merged.add(line))
    })

    if (merged.size === 0) {
      throw new Error('동기화된 트래커 항목이 없습니다.')
    }

    settingTrackerList.value = Array.from(merged).join('\n')
    settingLastSyncTrackerTime.value = Date.now()
    saveSettingsToStorage()
    pushSuccess(`트래커 ${merged.size}개를 동기화했습니다.`)
  } catch (error) {
    pushError(String(error))
  } finally {
    trackerSyncing.value = false
  }
}

function checkUpdateNow() {
  settingLastCheckUpdateTime.value = Date.now()
  saveSettingsToStorage()
  pushSuccess('업데이트 확인을 실행했습니다. (업데이터 연동은 후속 구현)')
}

function fillUserAgentPreset(kind: 'aria2' | 'transmission' | 'chrome') {
  if (kind === 'aria2') {
    settingUserAgent.value = 'aria2/1.37.0'
    return
  }
  if (kind === 'transmission') {
    settingUserAgent.value = 'Transmission/4.0.6'
    return
  }
  settingUserAgent.value = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122 Safari/537.36'
}

function buildRuntimeGlobalOptions(): Record<string, string> {
  const proxyServer = settingProxyEnabled.value ? settingProxyServer.value.trim() : ''
  return {
    'max-overall-upload-limit': toLimitBytesPerSecond(settingUploadLimit.value),
    'max-overall-download-limit': toLimitBytesPerSecond(settingDownloadLimit.value),
    'max-concurrent-downloads': String(Math.max(1, Math.round(maxConcurrentDownloads.value))),
    split: String(Math.max(1, Math.round(split.value))),
    'all-proxy': proxyServer,
    'no-proxy': settingProxyEnabled.value ? settingProxyBypass.value.trim() : '',
    'bt-save-metadata': boolToAria(settingMagnetAsTorrent.value),
    'bt-force-encryption': boolToAria(settingBtForceEncryption.value),
    'pause-metadata': boolToAria(!settingAutoDownloadTorrentMeta.value),
    'seed-ratio': settingKeepSeeding.value ? String(Math.max(0, settingSeedRatio.value)) : '0',
    'bt-tracker': trackerTextToAria2Value(settingTrackerList.value),
    'listen-port': String(sanitizePort(settingBtListenPort.value, 21301)),
    'dht-listen-port': String(sanitizePort(settingDhtListenPort.value, 26701)),
    'enable-dht': boolToAria(settingProtocolMagnet.value),
    'enable-dht6': boolToAria(settingProtocolMagnet.value),
    'enable-peer-exchange': boolToAria(settingProtocolMagnet.value),
    'enable-upnp': boolToAria(settingEnableUpnp.value),
    'user-agent': settingUserAgent.value.trim(),
    'check-certificate': boolToAria(!settingSkipTlsVerify.value),
  }
}

async function saveSettings() {
  const rpcChanged = activeRpcPort.value !== rpcPort.value || activeRpcSecret.value !== rpcSecret.value.trim()
  settingTrackerSources.value = normalizeLineItems(settingTrackerSourceInput.value)
  saveSettingsToStorage()

  if (rpcChanged && status.value.running) {
    if (status.value.binaryPath?.startsWith('external://')) {
      pushError('외부 aria2 재사용 상태에서는 RPC 포트/비밀 즉시 적용이 불가합니다. 외부 aria2를 재시작하세요.')
      return
    }

    try {
      await stopEngine()
      const restarted = await autoStartEngine(true)
      if (!restarted) {
        pushError('RPC 설정 적용을 위해 엔진 재시작에 실패했습니다.')
        return
      }
      await refreshTasks()
      pushSuccess('RPC 설정을 적용하기 위해 엔진을 재시작했습니다.')
    } catch (error) {
      pushError(`RPC 설정 반영 실패: ${error}`)
    }
    return
  }

  if (!status.value.running) {
    activeRpcPort.value = rpcPort.value
    activeRpcSecret.value = rpcSecret.value.trim()
    pushSuccess('설정을 저장했습니다. 엔진 시작 시 반영됩니다.')
    return
  }

  try {
    await changeAria2GlobalOption({
      rpc: rpcConfig(),
      options: buildRuntimeGlobalOptions(),
    })
    pushSuccess('설정을 저장하고 즉시 적용했습니다. 일부 항목은 새 작업/재시작 후 완전 반영됩니다.')
  } catch (error) {
    pushError(`설정은 저장됐지만 런타임 적용에 실패했습니다: ${error}`)
  }
}

async function pickDownloadFolder() {
  try {
    const selected = await openDialog({
      directory: true,
      multiple: false,
      defaultPath: downloadDir.value || undefined,
      title: '기본 다운로드 폴더 선택',
    })
    if (typeof selected === 'string' && selected.trim()) {
      downloadDir.value = selected
      saveSettingsToStorage()
      pushSuccess('기본 폴더를 변경했습니다.')
    }
  } catch (error) {
    pushError(String(error))
  }
}

function closeAddModal() {
  showAddModal.value = false
  modalDropActive.value = false
  addShowAdvanced.value = false
}

function openAddModalForTorrent() {
  showAddModal.value = true
  addTab.value = 'torrent'
}

async function toBase64(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = () => {
      const result = reader.result
      if (typeof result !== 'string') {
        reject(new Error('파일을 읽을 수 없습니다.'))
        return
      }
      const base64 = result.includes(',') ? (result.split(',')[1] ?? '') : result
      resolve(base64)
    }
    reader.onerror = () => reject(new Error('파일 읽기 실패'))
    reader.readAsDataURL(file)
  })
}

function fileExt(name: string): string {
  const idx = name.lastIndexOf('.')
  if (idx < 0) return '-'
  return name.slice(idx + 1).toLowerCase()
}

async function applyTorrentFile(file: File) {
  if (!/\.torrent$/i.test(file.name)) {
    pushError('torrent 파일만 추가할 수 있습니다.')
    return
  }

  torrentBase64.value = await toBase64(file)
  torrentFileName.value = file.name
  torrentFileSize.value = file.size
  torrentFileExt.value = fileExt(file.name)

  if (!addOut.value) {
    addOut.value = file.name.replace(/\.torrent$/i, '')
  }

  openAddModalForTorrent()
}

async function applyTorrentPath(path: string) {
  const payload = await loadTorrentFile(path)
  torrentBase64.value = payload.base64
  torrentFileName.value = payload.name
  torrentFileSize.value = payload.size
  torrentFileExt.value = fileExt(payload.name)
  if (!addOut.value) {
    addOut.value = payload.name.replace(/\.torrent$/i, '')
  }
  openAddModalForTorrent()
}

function triggerTorrentPick() {
  torrentFileInput.value?.click()
}

async function onTorrentFileChange(event: Event) {
  const input = event.target as HTMLInputElement
  const file = input.files?.[0]
  if (!file) return

  try {
    await applyTorrentFile(file)
  } catch (error) {
    pushError(String(error))
  }
}

async function onModalTorrentDrop(event: DragEvent) {
  event.preventDefault()
  modalDropActive.value = false

  const file = event.dataTransfer?.files?.[0]
  if (!file) return

  try {
    await applyTorrentFile(file)
  } catch (error) {
    pushError(String(error))
  }
}

function onModalTorrentDragOver(event: DragEvent) {
  event.preventDefault()
  modalDropActive.value = true
}

function onModalTorrentDragLeave() {
  modalDropActive.value = false
}

function extractDroppedFile(event: DragEvent): File | null {
  const files = event.dataTransfer?.files
  if (files && files.length > 0) {
    return files[0] ?? null
  }

  const items = event.dataTransfer?.items
  if (!items) {
    return null
  }
  for (const item of Array.from(items)) {
    const file = item.getAsFile()
    if (file) {
      return file
    }
  }
  return null
}

function normalizeTaskUri(uri: string): string {
  const trimmed = uri.trim()
  if (!trimmed) return ''

  if (trimmed.startsWith('magnet:') || trimmed.startsWith('thunder:')) {
    return trimmed
  }

  try {
    return new URL(trimmed).toString()
  } catch {
    if (/^(https?|ftp):\/\//i.test(trimmed)) {
      return trimmed.replace(/ /g, '%20')
    }
    return trimmed
  }
}

function guessFileNameFromUri(uri: string): string {
  try {
    const parsed = new URL(uri)
    const path = parsed.pathname || ''
    const name = path.split('/').filter(Boolean).pop() || ''
    return decodeURIComponent(name) || uri
  } catch {
    return uri
  }
}

function prependPendingTask(gid: string, fileName: string, uri: string) {
  const exists = [...tasks.value.active, ...tasks.value.waiting, ...tasks.value.stopped].some((task) => task.gid === gid)
  if (exists) return
  tasks.value.active.unshift({
    gid,
    status: 'active',
    totalLength: '0',
    completedLength: '0',
    downloadSpeed: '0',
    dir: downloadDir.value.trim() || '-',
    fileName: fileName.trim() || '-',
    uri: uri.trim(),
  })
}

function onWindowDragEnter(event: DragEvent) {
  event.preventDefault()
  appDropActive.value = true
}

function onWindowDragOver(event: DragEvent) {
  event.preventDefault()
  appDropActive.value = true
}

function onWindowDragLeave() {
  appDropActive.value = false
}

async function onWindowDrop(event: DragEvent) {
  event.preventDefault()
  event.stopPropagation()
  appDropActive.value = false

  const file = extractDroppedFile(event)
  if (!file) return

  try {
    await applyTorrentFile(file)
  } catch (error) {
    pushError(String(error))
  }
}

async function onSubmitAddTask() {
  const ready = await ensureEngineRunning()
  if (!ready) return

  loadingAddTask.value = true
  try {
    const headers = [
      addUserAgent.value.trim() ? `User-Agent: ${addUserAgent.value.trim()}` : '',
      addReferer.value.trim() ? `Referer: ${addReferer.value.trim()}` : '',
      addCookie.value.trim() ? `Cookie: ${addCookie.value.trim()}` : '',
      addAuthorization.value.trim() ? `Authorization: ${addAuthorization.value.trim()}` : '',
    ].filter((line) => line.length > 0)

    const addOptions: Record<string, string | string[]> = {}
    if (addProxy.value.trim()) {
      addOptions['all-proxy'] = addProxy.value.trim()
    }
    if (addUserAgent.value.trim()) {
      addOptions['user-agent'] = addUserAgent.value.trim()
    }
    addOptions['check-certificate'] = settingSkipTlsVerify.value ? 'false' : 'true'
    if (headers.length > 0) {
      addOptions.header = headers
    }

    if (addTab.value === 'url') {
      const uris = addUrl.value
        .split('\n')
        .map((line) => normalizeTaskUri(line))
        .filter((line) => line.length > 0)
      if (uris.length === 0) {
        pushError('URL을 입력하세요.')
        return
      }
      const gids: string[] = []
      for (const uri of uris) {
        const gid = await addAria2Uri({
          rpc: rpcConfig(),
          uri,
          out: uris.length === 1 ? (addOut.value.trim() || undefined) : undefined,
          dir: downloadDir.value.trim() || undefined,
          split: addSplit.value,
          options: Object.keys(addOptions).length > 0 ? addOptions : undefined,
        })
        gids.push(gid)
        void inspectAddedTask(gid)
        prependPendingTask(gid, uris.length === 1 ? addOut.value || guessFileNameFromUri(uri) : guessFileNameFromUri(uri), uri)
      }
      pushSuccess(`${gids.length}개 작업이 추가되었습니다.`)
    } else {
      if (!torrentBase64.value.trim()) {
        pushError('.torrent 파일을 선택하세요.')
        return
      }
      const gid = await addAria2Torrent({
        rpc: rpcConfig(),
        torrentBase64: torrentBase64.value,
        out: addOut.value.trim() || undefined,
        dir: downloadDir.value.trim() || undefined,
        split: addSplit.value,
        options: Object.keys(addOptions).length > 0 ? addOptions : undefined,
      })
      pushSuccess(`토렌트가 추가되었습니다. gid=${gid}`)
      void inspectAddedTask(gid)
      prependPendingTask(gid, addOut.value || torrentFileName.value.replace(/\.torrent$/i, ''), '')
    }

    if (addNavigateToDownloading.value) {
      page.value = 'downloads'
      filter.value = 'active'
    }

    addUrl.value = ''
    addOut.value = ''
    addShowAdvanced.value = false
    addUserAgent.value = ''
    addAuthorization.value = ''
    addReferer.value = ''
    addCookie.value = ''
    addProxy.value = ''
    addNavigateToDownloading.value = true
    torrentBase64.value = ''
    torrentFileName.value = ''
    torrentFileExt.value = ''
    torrentFileSize.value = 0
    showAddModal.value = false
    await refreshTasks()
  } catch (error) {
    pushError(String(error))
  } finally {
    loadingAddTask.value = false
  }
}

onMounted(async () => {
  loadSettingsFromStorage()
  activeRpcPort.value = rpcPort.value
  activeRpcSecret.value = rpcSecret.value.trim()
  await refreshEngineStatus()
  if (!status.value.running) {
    await autoStartEngine(true)
  }
  await refreshTasks()
  if (status.value.running) {
    try {
      await changeAria2GlobalOption({
        rpc: rpcConfig(),
        options: buildRuntimeGlobalOptions(),
      })
    } catch {
      // ignore bootstrap sync failures
    }
  }
  updateRefreshTimer()

  window.addEventListener('dragover', onWindowDragOver)
  window.addEventListener('dragenter', onWindowDragEnter)
  window.addEventListener('dragleave', onWindowDragLeave)
  window.addEventListener('drop', onWindowDrop)

  unlistenDragDrop = await getCurrentWindow().onDragDropEvent(async (event) => {
    if (event.payload.type === 'over' || event.payload.type === 'enter') {
      appDropActive.value = true
      return
    }
    if (event.payload.type === 'leave') {
      appDropActive.value = false
      return
    }
    if (event.payload.type === 'drop') {
      appDropActive.value = false
      const droppedPath = event.payload.paths[0]
      if (!droppedPath) return
      try {
        await applyTorrentPath(droppedPath)
      } catch (error) {
        pushError(String(error))
      }
    }
  })

  try {
    const startupDeepLinks = await getCurrentDeepLink()
    await handleDeepLinkUrls(startupDeepLinks)
    unlistenDeepLink = await onOpenUrl(async (urls) => {
      await handleDeepLinkUrls(urls)
    })
  } catch {
    // ignore deep-link bootstrap failures
  }

  if (externalAddPollTimer !== null) {
    window.clearInterval(externalAddPollTimer)
  }
  externalAddPollTimer = window.setInterval(() => {
    void pollExternalAddQueue()
  }, 700)
  void pollExternalAddQueue()
})

onUnmounted(() => {
  if (refreshTimer !== null) {
    window.clearInterval(refreshTimer)
  }

  window.removeEventListener('dragover', onWindowDragOver)
  window.removeEventListener('dragenter', onWindowDragEnter)
  window.removeEventListener('dragleave', onWindowDragLeave)
  window.removeEventListener('drop', onWindowDrop)

  if (unlistenDragDrop) {
    unlistenDragDrop()
    unlistenDragDrop = null
  }

  if (unlistenDeepLink) {
    unlistenDeepLink()
    unlistenDeepLink = null
  }

  if (toastTimer !== null) {
    window.clearTimeout(toastTimer)
    toastTimer = null
  }

  if (externalAddPollTimer !== null) {
    window.clearInterval(externalAddPollTimer)
    externalAddPollTimer = null
  }
})
</script>

<template>
  <main class="app-shell" :class="{ 'app-drop-active': appDropActive }">
    <aside class="sidebar">
      <div class="brand">m</div>
      <div class="side-icons">
        <button class="side-icon" :class="{ active: page === 'downloads' }" title="목록" @click="openDownloadsPage">
          ☰
        </button>
        <button class="side-icon add" title="새 작업" @click="openAddModal">＋</button>
      </div>
      <div class="sidebar-foot">
        <button class="side-icon" :class="{ active: page === 'settings' }" title="설정" @click="openSettingsPage">
          ⚙
        </button>
        <button class="side-icon" title="도움말">?</button>
      </div>
    </aside>

    <section v-if="page === 'downloads'" class="content downloads-view">
      <aside class="downloads-filter card">
        <h2>작업</h2>
        <button :class="{ active: filter === 'active' }" @click="filter = 'active'">▶ 다운로드 중</button>
        <button :class="{ active: filter === 'waiting' }" @click="filter = 'waiting'">⏸ 대기 중</button>
        <button :class="{ active: filter === 'stopped' }" @click="filter = 'stopped'">■ 중단됨</button>
        <button :class="{ active: filter === 'completed' }" @click="filter = 'completed'">✓ 다운로드 완료</button>
      </aside>

      <section class="downloads-main">
        <header class="toolbar">
          <div class="toolbar-left">
            <h1>
              {{
                filter === 'active'
                  ? '다운로드 중'
                  : filter === 'waiting'
                    ? '대기 중'
                    : filter === 'stopped'
                      ? '중단됨'
                      : filter === 'completed'
                        ? '다운로드 완료'
                        : '작업'
              }}
            </h1>
          </div>
          <div class="toolbar-right">
            <button
              class="icon-tool ghost"
              :disabled="loadingTaskAction || filteredTaskCount === 0"
              title="목록 삭제"
              @click="onRemoveFilteredTasks"
            >
              ✕
            </button>
            <button class="icon-tool ghost" :disabled="loadingTasks" title="새로고침" @click="refreshTasks()">↻</button>
            <button class="icon-tool ghost" :disabled="loadingTaskAction || !status.running" title="전체 재개" @click="onResumeAllTasks">▶</button>
            <button class="icon-tool ghost" :disabled="loadingTaskAction || !status.running" title="전체 일시정지" @click="onPauseAllTasks">⏸</button>
          </div>
        </header>

        <section v-if="errorMessage" class="notice error">{{ errorMessage }}</section>
        <section v-if="successMessage" class="notice success">{{ successMessage }}</section>

        <article class="task-pane card">
          <div class="task-card-list">
            <article v-for="task in filteredTasks" :key="task.gid" class="task-card">
              <div class="task-card-head">
                <div class="file-cell">
                  <strong>{{ task.fileName || '-' }}</strong>
                </div>
                <div class="task-actions-pill">
                  <button
                    class="icon-pill ghost"
                    :disabled="loadingTaskAction || task.status === 'stopped'"
                    :title="taskPrimaryTitle(task)"
                    @click="onTaskAction(task, taskPrimaryAction(task))"
                  >
                    {{ taskPrimaryIcon(task) }}
                  </button>
                  <button
                    class="icon-pill ghost"
                    :disabled="loadingTaskAction"
                    title="삭제"
                    @click="onTaskAction(task, task.status === 'stopped' ? 'purge' : 'remove')"
                  >
                    ✕
                  </button>
                  <button class="icon-pill ghost" title="폴더 열기" @click="onOpenTaskFolder(task)">📁</button>
                  <button class="icon-pill ghost" :title="task.uri ? '링크 복사' : 'GID 복사'" @click="onCopyTaskLink(task)">
                    🔗
                  </button>
                  <button class="icon-pill ghost" title="정보" @click="onShowTaskInfo(task)">ⓘ</button>
                </div>
              </div>

              <div class="progress-row">
                <div class="bar">
                  <span :style="{ width: `${progress(task).toFixed(1)}%` }" />
                </div>
              </div>

              <div class="task-meta-row">
                <span>{{ formatBytes(task.completedLength) }} / {{ formatBytes(task.totalLength) }}</span>
                <span class="task-meta-right">
                  <span>{{ formatSpeed(task.downloadSpeed) }}</span>
                </span>
              </div>
            </article>

            <div v-if="filteredTasks.length === 0" class="empty">현재 다운로드 없음</div>
          </div>
        </article>

        <transition name="task-info-slide">
          <aside v-if="taskInfoVisible" class="task-info-panel card">
            <header class="task-info-header">
              <h3>작업 정보</h3>
              <button class="icon-btn" @click="closeTaskInfo">×</button>
            </header>

            <div class="task-info-tabs">
              <button :class="{ active: taskInfoTab === 'general' }" title="일반" @click="taskInfoTab = 'general'">●</button>
              <button :class="{ active: taskInfoTab === 'activity' }" title="활동" @click="taskInfoTab = 'activity'">▦</button>
              <button :class="{ active: taskInfoTab === 'trackers' }" title="트래커" @click="taskInfoTab = 'trackers'">⊘</button>
              <button :class="{ active: taskInfoTab === 'peers' }" title="피어" @click="taskInfoTab = 'peers'">♟</button>
              <button :class="{ active: taskInfoTab === 'files' }" title="파일" @click="taskInfoTab = 'files'">▣</button>
            </div>

            <section v-if="taskInfoLoading && !taskInfoDetail" class="task-info-body">
              <div class="empty">정보를 불러오는 중...</div>
            </section>

            <section v-else class="task-info-body">
              <template v-if="taskInfoTab === 'general'">
                <div class="task-info-grid">
                  <div class="k">작업 이름</div>
                  <div>{{ taskInfoLiveTask?.fileName || taskInfoTask?.fileName || '-' }}</div>
                  <div class="k">폴더</div>
                  <div class="inline-row">
                    <span>{{ taskInfoLiveTask?.dir || taskInfoDetail?.dir || '-' }}</span>
                    <button class="icon-pill ghost" @click="taskInfoLiveTask && onOpenTaskFolder(taskInfoLiveTask)">📁</button>
                  </div>
                  <div class="k">상태</div>
                  <div>
                    <span class="status-tag" :class="taskInfoLiveTask?.status || taskInfoDetail?.status || 'stopped'">
                      {{ statusLabel(taskInfoLiveTask?.status || taskInfoDetail?.status) }}
                    </span>
                  </div>
                </div>
                <div class="task-info-divider">토렌트 정보</div>
                <div class="task-info-grid">
                  <div class="k">해시</div>
                  <div>{{ taskInfoDetail?.infoHash || '-' }}</div>
                  <div class="k">조각 크기</div>
                  <div>{{ formatBytes(taskInfoDetail?.pieceLength || '0') }}</div>
                  <div class="k">조각</div>
                  <div>{{ taskInfoDetail?.numPieces || '-' }}</div>
                  <div class="k">제작일</div>
                  <div>{{ formatUnixSeconds(taskInfoDetail?.creationDate || '') }}</div>
                  <div class="k">코멘트</div>
                  <div>{{ taskInfoDetail?.comment || '-' }}</div>
                </div>
              </template>

              <template v-else-if="taskInfoTab === 'activity'">
                <div class="piece-grid">
                  <span v-for="idx in 96" :key="idx" :class="{ done: idx / 96 <= taskInfoProgress / 100 }"></span>
                </div>
                <div class="task-info-progress-row">
                  <span>진행</span>
                  <div class="bar"><span :style="{ width: `${taskInfoProgress.toFixed(1)}%` }" /></div>
                  <strong>{{ taskInfoProgress.toFixed(0) }}%</strong>
                </div>
                <div class="task-info-grid">
                  <div class="k">용량</div>
                  <div>{{ formatBytes(taskInfoDetail?.completedLength || '0') }} / {{ formatBytes(taskInfoDetail?.totalLength || '0') }}</div>
                  <div class="k">시더</div>
                  <div>{{ taskInfoDetail?.numSeeders || '0' }}</div>
                  <div class="k">연결</div>
                  <div>{{ taskInfoDetail?.connections || '0' }}</div>
                  <div class="k">다운로드 속도</div>
                  <div>{{ formatSpeed(taskInfoDetail?.downloadSpeed || '0') }}</div>
                  <div class="k">업로드 속도</div>
                  <div>{{ formatSpeed(taskInfoDetail?.uploadSpeed || '0') }}</div>
                  <div class="k">업로드됨</div>
                  <div>{{ formatBytes(taskInfoDetail?.uploadLength || '0') }}</div>
                  <div class="k">비율</div>
                  <div>{{ taskInfoRatio }}</div>
                </div>
              </template>

              <template v-else-if="taskInfoTab === 'trackers'">
                <div class="task-info-list">
                  <div v-for="tracker in taskInfoDetail?.trackers || []" :key="tracker">{{ tracker }}</div>
                  <div v-if="(taskInfoDetail?.trackers?.length || 0) === 0" class="empty">트래커 정보 없음</div>
                </div>
              </template>

              <template v-else-if="taskInfoTab === 'peers'">
                <table class="task-info-table">
                  <thead>
                    <tr>
                      <th>호스트</th>
                      <th>클라이언트</th>
                      <th>%</th>
                      <th>↓</th>
                      <th>↑</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr v-for="peer in taskInfoDetail?.peers || []" :key="`${peer.ip}:${peer.port}`">
                      <td>{{ peer.ip }}:{{ peer.port }}</td>
                      <td>{{ parsePeerClient(peer.peerId) }}</td>
                      <td>{{ peerPercent(peer, taskInfoDetail) }}</td>
                      <td>{{ formatSpeed(peer.downloadSpeed) }}</td>
                      <td>{{ formatSpeed(peer.uploadSpeed) }}</td>
                    </tr>
                    <tr v-if="(taskInfoDetail?.peers?.length || 0) === 0">
                      <td colspan="5" class="empty">데이터 없음</td>
                    </tr>
                  </tbody>
                </table>
              </template>

              <template v-else>
                <table class="task-info-table">
                  <thead>
                    <tr>
                      <th>파일 이름</th>
                      <th>확장자</th>
                      <th>%</th>
                      <th>✓</th>
                      <th>크기</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr v-for="file in taskInfoDetail?.files || []" :key="file.path">
                      <td>{{ fileNameFromPath(file.path) }}</td>
                      <td>{{ fileExtFromPath(file.path) }}</td>
                      <td>{{ Number(file.length) > 0 ? ((Number(file.completedLength) / Number(file.length)) * 100).toFixed(0) : '0' }}</td>
                      <td>{{ file.selected === 'true' || file.selected === '1' ? '✓' : '-' }}</td>
                      <td>{{ formatBytes(file.length) }}</td>
                    </tr>
                    <tr v-if="(taskInfoDetail?.files?.length || 0) === 0">
                      <td colspan="5" class="empty">파일 정보 없음</td>
                    </tr>
                  </tbody>
                </table>
                <div class="task-info-files-meta">{{ taskInfoSelectedFilesText }}</div>
              </template>
            </section>

            <footer class="task-info-footer">
              <div class="task-info-actions-pill">
                <button
                  class="icon-pill ghost"
                  :disabled="loadingTaskAction || !taskInfoActionTask || taskInfoActionTask.status === 'stopped'"
                  :title="taskInfoActionTask ? taskPrimaryTitle(taskInfoActionTask) : '재개/정지'"
                  @click="onTaskInfoPrimaryAction"
                >
                  {{ taskInfoActionTask ? taskPrimaryIcon(taskInfoActionTask) : '□' }}
                </button>
                <button
                  class="icon-pill ghost"
                  :disabled="loadingTaskAction || !taskInfoActionTask"
                  title="삭제"
                  @click="onTaskInfoRemove"
                >
                  ✕
                </button>
                <button
                  class="icon-pill ghost"
                  title="폴더 열기"
                  :disabled="!taskInfoActionTask"
                  @click="taskInfoActionTask && onOpenTaskFolder(taskInfoActionTask)"
                >
                  📁
                </button>
                <button
                  class="icon-pill ghost"
                  :title="taskInfoActionTask?.uri ? '링크 복사' : 'GID 복사'"
                  :disabled="!taskInfoActionTask"
                  @click="taskInfoActionTask && onCopyTaskLink(taskInfoActionTask)"
                >
                  🔗
                </button>
              </div>
            </footer>
          </aside>
        </transition>
      </section>
    </section>

    <section v-else class="content settings-content">
      <section class="settings-layout">
        <aside class="settings-nav card">
          <h2>Settings</h2>
          <p class="settings-nav-sub">다운로드 엔진과 앱 동작을 조정합니다.</p>
          <button :class="{ active: settingsTab === 'basic' }" @click="settingsTab = 'basic'">
            <span>기본</span>
            <small>일반 사용 옵션</small>
          </button>
          <button :class="{ active: settingsTab === 'advanced' }" @click="settingsTab = 'advanced'">
            <span>고급</span>
            <small>네트워크/RPC</small>
          </button>
          <button :class="{ active: settingsTab === 'lab' }" @click="settingsTab = 'lab'">
            <span>실험실</span>
            <small>미리보기 기능</small>
          </button>
        </aside>

        <article class="settings-panel card">
          <header class="settings-header">
            <h1>환경 설정</h1>
            <p>Motrix 스타일에 맞춘 엔진/다운로드 환경 구성</p>
          </header>

          <div v-if="settingsTab === 'basic'" class="settings-form">
            <section class="settings-group settings-card-section">
              <div class="group-title">UI & 표시</div>
              <label class="field-label">테마</label>
              <div class="theme-row">
                <button class="theme-tile" :class="{ active: settingTheme === 'auto' }" @click="settingTheme = 'auto'">자동</button>
                <button class="theme-tile" :class="{ active: settingTheme === 'light' }" @click="settingTheme = 'light'">밝게</button>
                <button class="theme-tile" :class="{ active: settingTheme === 'dark' }" @click="settingTheme = 'dark'">어둡게</button>
              </div>
            </section>

            <section class="settings-group settings-card-section">
              <div class="group-title">시작/표시 동작</div>
              <label class="check-row"><input v-model="settingHideWindowOnStartup" type="checkbox" /> 창 자동으로 숨기기</label>
              <label class="check-row"><input v-model="settingTraySpeed" type="checkbox" /> 메뉴 막대 트레이에 실시간 속도 표시</label>
              <label class="check-row"><input v-model="settingShowDockProgress" type="checkbox" /> 다운로드 진행률 막대 보기</label>
            </section>

            <section class="settings-group settings-card-section two-col">
              <div class="group-title full-row">기본 파라미터</div>
              <label>언어
                <select v-model="settingLanguage">
                  <option>한국어</option>
                  <option>English</option>
                </select>
              </label>
              <label>분할
                <input v-model.number="split" type="number" min="1" max="64" />
              </label>
            </section>

            <section class="settings-group settings-card-section">
              <div class="group-title">세션</div>
              <label class="check-row"><input v-model="settingRunOnLogin" type="checkbox" /> 로그인 시 실행</label>
              <label class="check-row"><input v-model="settingRememberWindow" type="checkbox" /> 창 크기 및 위치 기억</label>
              <label class="check-row"><input v-model="settingAutoResume" type="checkbox" /> 완료되지 않은 작업 자동 재개</label>
            </section>

            <section class="settings-group settings-card-section">
              <div class="group-title">다운로드 디렉터리</div>
              <label class="field-label">기본 폴더</label>
              <div class="folder-picker-row">
                <input v-model="downloadDir" type="text" placeholder="/Users/.../Downloads" />
                <button type="button" class="ghost folder-browse-btn" @click.stop.prevent="pickDownloadFolder">
                  폴더 선택
                </button>
              </div>
            </section>

            <section class="settings-group settings-card-section two-col">
              <div class="group-title full-row">대역폭 제한</div>
              <label>업로드 제한 (KB/s)
                <input v-model.number="settingUploadLimit" type="number" min="0" />
              </label>
              <label>다운로드 제한 (KB/s)
                <input v-model.number="settingDownloadLimit" type="number" min="0" />
              </label>
            </section>

            <section class="settings-group settings-card-section">
              <div class="group-title">BitTorrent</div>
              <label class="check-row"><input v-model="settingMagnetAsTorrent" type="checkbox" /> 마그넷 링크를 토렌트 파일로 저장</label>
              <label class="check-row"><input v-model="settingAutoDownloadTorrentMeta" type="checkbox" /> 마그넷 및 토렌트 내용 자동 다운로드</label>
              <label class="check-row"><input v-model="settingBtForceEncryption" type="checkbox" /> BT 강제 암호화</label>
              <label class="check-row"><input v-model="settingKeepSeeding" type="checkbox" /> 수동으로 멈출 때까지 계속 배포</label>
              <label>배포 비율
                <input v-model.number="settingSeedRatio" type="number" min="0" step="0.1" />
              </label>
            </section>

            <div class="settings-actions">
              <button @click="saveSettings">저장 및 적용</button>
              <button class="ghost" @click="openDownloadsPage">취소</button>
            </div>
          </div>

          <div v-else-if="settingsTab === 'advanced'" class="settings-form">
            <section class="settings-group settings-card-section">
              <div class="group-title">자동 업데이트</div>
              <label class="check-row"><input v-model="settingAutoCheckUpdate" type="checkbox" /> 자동으로 업데이트 확인</label>
              <div class="inline-action-row">
                <small class="muted-line">마지막 확인: {{ lastCheckUpdateTimeText }}</small>
                <button class="ghost tiny" @click="checkUpdateNow">지금 확인</button>
              </div>
            </section>

            <section class="settings-group settings-card-section">
              <div class="group-title">프록시</div>
              <label class="check-row"><input v-model="settingProxyEnabled" type="checkbox" /> 프록시 사용</label>
              <template v-if="settingProxyEnabled">
                <label>프록시 서버
                  <input v-model="settingProxyServer" type="text" placeholder="[http://][USER:PASSWORD@]HOST[:PORT]" />
                </label>
                <label>우회 주소 (한 줄에 하나)
                  <textarea v-model="settingProxyBypass" rows="2" placeholder="localhost&#10;127.0.0.1" />
                </label>
              </template>
            </section>

            <section class="settings-group settings-card-section">
              <div class="group-title">트래커 서버</div>
              <label>트래커 소스 (한 줄에 하나)
                <textarea v-model="settingTrackerSourceInput" rows="2" />
              </label>
              <div class="inline-action-row">
                <small class="muted-line">권장: ngosang/trackerslist, XIU2/TrackersListCollection</small>
                <button class="ghost tiny" :disabled="trackerSyncing" @click="syncTrackerFromSources">
                  {{ trackerSyncing ? '동기화 중...' : '동기화' }}
                </button>
              </div>
              <label>트래커 목록 (한 줄에 하나)
                <textarea v-model="settingTrackerList" rows="4" placeholder="udp://.../announce" />
              </label>
              <label class="check-row"><input v-model="settingAutoSyncTracker" type="checkbox" /> 매일 자동으로 트래커 목록 업데이트</label>
              <small class="muted-line">마지막 동기화: {{ lastSyncTrackerTimeText }}</small>
            </section>

            <section class="settings-group settings-card-section two-col">
              <div class="group-title full-row">RPC</div>
              <label>RPC 청취 포트
                <div class="input-action-row">
                  <input v-model.number="rpcPort" type="number" min="1" max="65535" />
                  <button class="ghost tiny" @click="rpcPort = randomPort()">랜덤</button>
                </div>
              </label>
              <label>RPC 비밀
                <div class="input-action-row">
                  <input v-model="rpcSecret" type="text" placeholder="RPC Secret" />
                  <button class="ghost tiny" @click="rpcSecret = randomSecret()">생성</button>
                </div>
              </label>
              <div class="rpc-helper full-row">
                <label>RPC URL
                  <div class="input-action-row">
                    <input :value="rpcEndpointText" type="text" readonly />
                    <button class="ghost tiny" @click="copyRpcEndpoint">복사</button>
                  </div>
                </label>
                <label>확장 토큰 형식
                  <div class="input-action-row">
                    <input :value="rpcTokenText || 'token:(RPC Secret 필요)'" type="text" readonly />
                    <button class="ghost tiny" :disabled="!rpcTokenText" @click="copyRpcToken">복사</button>
                  </div>
                </label>
                <div class="rpc-test-row">
                  <button class="ghost tiny" :disabled="rpcTestLoading" @click="testRpcConnection">
                    {{ rpcTestLoading ? '테스트 중...' : '연결 테스트' }}
                  </button>
                  <small class="muted-line" :class="{ 'rpc-ok': rpcTestStatus === 'ok', 'rpc-fail': rpcTestStatus === 'fail' }">
                    {{ rpcTestStatus === 'idle' ? '확장 설정: URL + token:SECRET' : rpcTestMessage }}
                  </small>
                </div>
              </div>
            </section>

            <section class="settings-group settings-card-section two-col">
              <div class="group-title full-row">청취 포트</div>
              <label class="check-row full-row"><input v-model="settingEnableUpnp" type="checkbox" /> UPnP / NAT-PMP</label>
              <label>BT 청취 포트
                <div class="input-action-row">
                  <input v-model.number="settingBtListenPort" type="number" min="1" max="65535" />
                  <button class="ghost tiny" @click="settingBtListenPort = randomPort()">랜덤</button>
                </div>
              </label>
              <label>DHT 청취 포트
                <div class="input-action-row">
                  <input v-model.number="settingDhtListenPort" type="number" min="1" max="65535" />
                  <button class="ghost tiny" @click="settingDhtListenPort = randomPort()">랜덤</button>
                </div>
              </label>
            </section>

            <section class="settings-group settings-card-section">
              <div class="group-title">프로토콜</div>
              <small class="muted-line">다음 프로토콜의 기본 클라이언트로 설정</small>
              <label class="check-row"><input v-model="settingProtocolMagnet" type="checkbox" /> 마그넷 [ magnet:// ]</label>
              <label class="check-row"><input v-model="settingProtocolThunder" type="checkbox" /> 썬더 [ thunder:// ]</label>
              <label class="check-row"><input v-model="settingSkipTlsVerify" type="checkbox" /> SSL 인증서 검증 건너뛰기 (호환성)</label>
            </section>

            <section class="settings-group settings-card-section">
              <div class="group-title">User-Agent</div>
              <label>모의 사용자 에이전트
                <textarea v-model="settingUserAgent" rows="2" placeholder="User-Agent" />
              </label>
              <div class="inline-action-row">
                <button class="ghost tiny" @click="fillUserAgentPreset('aria2')">Aria2</button>
                <button class="ghost tiny" @click="fillUserAgentPreset('transmission')">Transmission</button>
                <button class="ghost tiny" @click="fillUserAgentPreset('chrome')">Chrome</button>
              </div>
            </section>

            <div class="settings-actions">
              <button @click="saveSettings">저장 및 적용</button>
              <button class="ghost" @click="openDownloadsPage">취소</button>
            </div>
          </div>

          <div v-else class="settings-placeholder card">
            <h3>실험실 설정</h3>
            <p>실험 기능과 실험적 네트워크 옵션을 단계적으로 이식합니다.</p>
          </div>
        </article>
      </section>
    </section>

    <div v-if="showAddModal" class="modal-backdrop" @click.self="closeAddModal">
      <section class="add-modal">
        <header class="modal-header">
          <div class="modal-tabs">
            <button :class="{ active: addTab === 'url' }" @click="addTab = 'url'">URL</button>
            <button :class="{ active: addTab === 'torrent' }" @click="addTab = 'torrent'">토렌트</button>
          </div>
          <button class="icon-btn" @click="closeAddModal">×</button>
        </header>

        <div class="modal-body">
          <template v-if="addTab === 'url'">
            <label>
              <span>URL</span>
              <textarea v-model="addUrl" placeholder="한 줄에 작업 URL 하나 (HTTP / FTP / Magnet)" rows="4" />
            </label>
          </template>

          <template v-else>
            <input ref="torrentFileInput" class="hidden-input" type="file" accept=".torrent" @change="onTorrentFileChange" />

            <div
              v-if="!torrentBase64"
              class="torrent-drop-zone"
              :class="{ active: modalDropActive }"
              @click="triggerTorrentPick"
              @dragover="onModalTorrentDragOver"
              @dragleave="onModalTorrentDragLeave"
              @drop="onModalTorrentDrop"
            >
              <p>토렌트 파일을 여기로 드래그하거나 클릭해서 선택하십시오</p>
            </div>

            <div v-else class="torrent-selected">
              <div class="torrent-head">
                <strong>{{ torrentFileName }}</strong>
                <button class="ghost small" @click="triggerTorrentPick">파일 변경</button>
              </div>

              <table class="torrent-file-table">
                <thead>
                  <tr>
                    <th></th>
                    <th>파일 이름</th>
                    <th>파일 확장자</th>
                    <th>파일 크기</th>
                  </tr>
                </thead>
                <tbody>
                  <tr>
                    <td><input type="checkbox" checked disabled /></td>
                    <td>{{ addOut || torrentFileName.replace(/\.torrent$/i, '') }}</td>
                    <td>{{ torrentFileExt || '-' }}</td>
                    <td>{{ formatBytesNumber(torrentFileSize) }}</td>
                  </tr>
                </tbody>
              </table>
            </div>
          </template>

          <div class="modal-row">
            <label>
              <span>이름 변경</span>
              <input v-model="addOut" type="text" placeholder="선택" />
            </label>
            <label>
              <span>분할</span>
              <input v-model.number="addSplit" type="number" min="1" max="64" />
            </label>
          </div>

          <label>
            <span>폴더</span>
            <input v-model="downloadDir" type="text" placeholder="/Users/.../Downloads" />
          </label>

          <transition name="expand-advanced">
            <div v-if="addShowAdvanced" class="modal-advanced">
              <label>
                <span>사용자 에이전트</span>
                <input v-model="addUserAgent" type="text" placeholder="사용자 에이전트" />
              </label>
              <label>
                <span>권한 부여</span>
                <input v-model="addAuthorization" type="text" placeholder="권한 부여" />
              </label>
              <label>
                <span>리퍼러</span>
                <input v-model="addReferer" type="text" placeholder="리퍼러" />
              </label>
              <label>
                <span>쿠키</span>
                <input v-model="addCookie" type="text" placeholder="쿠키" />
              </label>
              <label>
                <span>프록시</span>
                <div class="proxy-help-row">
                  <input v-model="addProxy" type="text" placeholder="[http://][USER:PASSWORD@]HOST[:PORT]" />
                  <a href="https://github.com/agalwood/Motrix/wiki/Proxy" target="_blank" rel="noopener noreferrer">프록시 매뉴얼 보기</a>
                </div>
              </label>
              <label class="check-row">
                <input v-model="addNavigateToDownloading" type="checkbox" />
                다운로드로 이동
              </label>
            </div>
          </transition>
        </div>

        <footer class="modal-footer">
          <label class="check-row modal-advanced-toggle">
            <input v-model="addShowAdvanced" type="checkbox" />
            고급 옵션
          </label>
          <div class="modal-footer-actions">
            <button class="ghost" @click="closeAddModal">취소</button>
            <button :disabled="loadingAddTask" @click="onSubmitAddTask">확인</button>
          </div>
        </footer>
      </section>
    </div>
    <div v-if="toastVisible" class="app-toast" :class="toastType">{{ toastMessage }}</div>
  </main>
</template>
