import { spawn, spawnSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

type NotifyAttempt = Readonly<{
  cmd: string
  args: string[]
  windowsHide?: boolean
}>

type SystemNotificationOptions = Readonly<{
  title: string
  message: string
  appName: string
  icon?: string
}>

type RuntimeProcess = Pick<NodeJS.Process, 'env' | 'execPath' | 'pid' | 'platform'>

type RuntimeGlobals = typeof globalThis & {
  process?: RuntimeProcess
}

type NodeNotifierCallback = (
  err: unknown,
  response?: unknown,
  metadata?: unknown,
) => void

type NodeNotifierLike = Readonly<{
  notify: (
    options: Record<string, unknown>,
    callback?: NodeNotifierCallback,
  ) => void
}>

export type SystemNotificationInteraction =
  | Readonly<{ type: 'reply'; value: string }>
  | Readonly<{ type: 'action'; value: string }>
  | Readonly<{ type: 'click' }>
  | Readonly<{ type: 'timeout' }>

type InteractiveSystemNotificationRequest = Readonly<{
  titleRaw: unknown
  messageRaw: unknown
  iconPathRaw?: unknown
  actions?: readonly string[]
  closeLabel?: string
  reply?: boolean
  timeoutSeconds?: number
  onInteraction?: (interaction: SystemNotificationInteraction) => void
}>

const SYSTEM_NOTIFICATION_APP_NAME = 'DimCode'
const NODE_NOTIFIER_CALLBACK_TIMEOUT_MS = 15000
const MAC_NOTIFICATION_SENDER_BY_TERM_PROGRAM = new Map<string, string>([
  ['Apple_Terminal', 'com.apple.Terminal'],
  ['iTerm.app', 'com.googlecode.iterm2'],
  ['iTerm2', 'com.googlecode.iterm2'],
  ['WezTerm', 'com.github.wez.wezterm'],
  ['Ghostty', 'com.mitchellh.ghostty'],
  ['ghostty', 'com.mitchellh.ghostty'],
  ['kitty', 'net.kovidgoyal.kitty'],
  ['Alacritty', 'org.alacritty'],
  ['Hyper', 'co.zeit.hyper'],
  ['WarpTerminal', 'dev.warp.Warp-Stable'],
])

function runtimeGlobals(): RuntimeGlobals {
  return globalThis as RuntimeGlobals
}

function runtimeProcessEnv(): Record<string, string | undefined> {
  return runtimeGlobals().process?.env ?? {}
}

function runtimeProcessPlatform(): string {
  return String(runtimeGlobals().process?.platform ?? '').trim()
}

function runtimeProcessExecPath(): string {
  return String(runtimeGlobals().process?.execPath ?? '').trim()
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return value != null && typeof value === 'object' && !Array.isArray(value)
}

function isNodeNotifierLike(value: unknown): value is NodeNotifierLike {
  return isRecord(value) && typeof value.notify === 'function'
}

function unrefTimer(timer: ReturnType<typeof setTimeout> | null): void {
  if (
    timer
    && typeof timer === 'object'
    && 'unref' in timer
    && typeof timer.unref === 'function'
  ) {
    timer.unref()
  }
}

function escapeAppleScriptString(value: string): string {
  return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
}

function toJsStringLiteral(value: string): string {
  return JSON.stringify(String(value ?? ''))
}

function escapePowerShellString(value: string): string {
  return value.replace(/'/g, "''")
}

function sanitizeNotificationBundleId(raw: unknown): string {
  const value = String(raw ?? '').trim()
  if (!value) return ''
  if (value.includes('\0')) return ''
  if (/\s/.test(value)) return ''
  if (!/^[\w\-.][-\w]*\.[\w\-.]+$/.test(value)) return ''
  return value
}

function extractMacAppBundlePath(commandRaw: unknown): string {
  const command = String(commandRaw ?? '').trim()
  if (!command) return ''
  const match = command.match(/(\/.*?\.app)(?:\/Contents\/MacOS\/\S+)?(?:\s|$)/)
  const appPath = String(match?.[1] ?? '').trim()
  if (!appPath || !existsSync(appPath)) return ''
  return appPath
}

function readMacAppBundleId(appPathRaw: unknown): string {
  const appPath = String(appPathRaw ?? '').trim()
  if (!appPath) return ''
  const infoRoot = path.join(appPath, 'Contents', 'Info')
  const infoPlist = `${infoRoot}.plist`
  if (!existsSync(infoPlist)) return ''
  const result = spawnSync(
    'defaults',
    ['read', infoRoot, 'CFBundleIdentifier'],
    {
      encoding: 'utf8',
    },
  )
  return sanitizeNotificationBundleId(result.stdout)
}

function resolveMacNotificationSenderFromProcessTree(): Readonly<{
  bundleId: string
  source: string
  appPath: string
}> | null {
  let pid = Number(runtimeGlobals().process?.pid ?? 0)
  if (!Number.isFinite(pid) || pid <= 0) return null

  for (let depth = 0; depth < 8; depth++) {
    const result = spawnSync(
      'ps',
      ['-p', String(pid), '-o', 'ppid=', '-o', 'command='],
      {
        encoding: 'utf8',
      },
    )
    const line =
      String(result.stdout ?? '')
        .split(/\r?\n/)
        .map((s) => s.trim())
        .find(Boolean) ?? ''
    const separatorIndex = line.search(/\s/)
    if (separatorIndex < 0) break
    const nextPid = Number(line.slice(0, separatorIndex).trim())
    const command = line.slice(separatorIndex).trim()
    const appPath = extractMacAppBundlePath(command)
    if (appPath) {
      const bundleId = readMacAppBundleId(appPath)
      if (bundleId) {
        return {
          bundleId,
          source: `process_tree:${depth}`,
          appPath,
        }
      }
    }
    if (!Number.isFinite(nextPid) || nextPid <= 0 || nextPid === pid) break
    pid = nextPid
  }

  return null
}

export function resolveMacNotificationSenderBundleId(
  env: Record<string, string | undefined>,
): string {
  if (env.DIMCODE_TEST === '1') return ''

  const override = sanitizeNotificationBundleId(env.DIMCODE_NOTIFICATION_SENDER)
  if (override) return override

  const termProgram = String(env.TERM_PROGRAM ?? '').trim()
  const lcTerminal = String(env.LC_TERMINAL ?? '').trim()
  const mapped =
    MAC_NOTIFICATION_SENDER_BY_TERM_PROGRAM.get(termProgram) ??
    MAC_NOTIFICATION_SENDER_BY_TERM_PROGRAM.get(lcTerminal)
  if (mapped) return mapped

  const fromProcessTree = resolveMacNotificationSenderFromProcessTree()
  if (fromProcessTree) return fromProcessTree.bundleId
  return ''
}

export function sanitizeNotificationText(raw: unknown, maxLen = 220): string {
  const replaceControlChars = (value: string): string => {
    let out = ''
    for (let i = 0; i < value.length; i++) {
      const code = value.charCodeAt(i)
      const isControl =
        (code >= 0 && code <= 8) || (code >= 11 && code <= 31) || code === 127
      out += isControl ? ' ' : value[i]!
    }
    return out
  }

  const normalized = replaceControlChars(String(raw ?? ''))
    .replace(/\s+/g, ' ')
    .trim()
  if (!normalized) return ''
  if (!Number.isFinite(maxLen) || maxLen <= 0) return normalized
  return normalized.length > maxLen
    ? normalized.slice(0, Math.max(1, Math.floor(maxLen)))
    : normalized
}

export function buildSystemNotificationOptions(
  titleRaw: unknown,
  messageRaw: unknown,
  iconPathRaw?: unknown,
): SystemNotificationOptions | null {
  const title = sanitizeNotificationText(titleRaw, 80)
  const message = sanitizeNotificationText(messageRaw, 280)
  const iconPath = sanitizeNotificationIconPath(iconPathRaw)
  if (!title || !message) return null

  return {
    title,
    message,
    appName: SYSTEM_NOTIFICATION_APP_NAME,
    ...(iconPath ? { icon: iconPath } : {}),
  }
}

export function buildNodeNotifierNotificationOptions(
  platform: string,
  titleRaw: unknown,
  messageRaw: unknown,
  iconPathRaw?: unknown,
  env?: Record<string, string | undefined>,
): Record<string, unknown> | null {
  const base = buildSystemNotificationOptions(titleRaw, messageRaw, iconPathRaw)
  if (!base) return null

  const shared = {
    title: base.title,
    message: base.message,
    ...(base.icon ? { icon: base.icon } : {}),
  }

  if (platform === 'darwin') {
    const sender = resolveMacNotificationSenderBundleId(
      env ?? runtimeProcessEnv(),
    )
    return {
      ...shared,
      ...(sender ? { sender, activate: sender } : {}),
    }
  }

  return {
    ...shared,
    appName: base.appName,
  }
}

export function buildSystemNotificationAttempts(
  platform: string,
  titleRaw: unknown,
  messageRaw: unknown,
  iconPathRaw?: unknown,
): NotifyAttempt[] {
  const options = buildSystemNotificationOptions(
    titleRaw,
    messageRaw,
    iconPathRaw,
  )
  if (!options) return []
  const { title, message, icon: iconPath } = options

  if (platform === 'darwin') {
    const jxa = `Application("System Events").displayNotification(${toJsStringLiteral(message)}, { withTitle: ${toJsStringLiteral(title)} })`
    const appleScript = `display notification "${escapeAppleScriptString(message)}" with title "${escapeAppleScriptString(title)}"`
    return [
      { cmd: 'osascript', args: ['-l', 'JavaScript', '-e', jxa] },
      { cmd: 'osascript', args: ['-e', appleScript] },
    ]
  }

  if (platform === 'win32') {
    const escapedTitle = escapePowerShellString(title)
    const escapedMessage = escapePowerShellString(message)
    const escapedIcon = iconPath
      ? escapePowerShellString(toWindowsFileUri(iconPath))
      : ''
    const toastScript = [
      "$ErrorActionPreference = 'Stop'",
      `$title = '${escapedTitle}'`,
      `$message = '${escapedMessage}'`,
      '$titleEsc = [System.Security.SecurityElement]::Escape($title)',
      '$messageEsc = [System.Security.SecurityElement]::Escape($message)',
      ...(escapedIcon ? [`$icon = '${escapedIcon}'`] : []),
      '[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null',
      '[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null',
      '$xml = New-Object Windows.Data.Xml.Dom.XmlDocument',
      `$xml.LoadXml("<toast><visual><binding template='ToastGeneric'><text>$titleEsc</text><text>$messageEsc</text>${
        escapedIcon
          ? "<image placement='appLogoOverride' hint-crop='circle' src='$icon'/>"
          : ''
      }</binding></visual></toast>")`,
      '$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)',
      "$notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('dimcode')",
      '$notifier.Show($toast)',
    ].join('; ')
    const args = [
      '-NoProfile',
      '-NonInteractive',
      '-ExecutionPolicy',
      'Bypass',
      '-Command',
      toastScript,
    ]
    return [
      { cmd: 'powershell.exe', args, windowsHide: true },
      { cmd: 'powershell', args, windowsHide: true },
      { cmd: 'pwsh', args, windowsHide: true },
    ]
  }

  if (platform === 'linux') {
    const args = iconPath ? ['-i', iconPath, title, message] : [title, message]
    return [{ cmd: 'notify-send', args }]
  }

  return []
}

function runNotifyAttempt(attempt: NotifyAttempt): Promise<boolean> {
  return new Promise<boolean>((resolve) => {
    try {
      const child = spawn(attempt.cmd, attempt.args, {
        stdio: 'ignore',
        windowsHide: Boolean(attempt.windowsHide),
      })
      child.on('error', () => resolve(false))
      child.on('close', (code) => resolve(code === 0))
    } catch {
      resolve(false)
    }
  })
}

async function loadNodeNotifier(): Promise<NodeNotifierLike | null> {
  try {
    const mod = await import('node-notifier')
    const candidate = mod.default ?? mod
    return isNodeNotifierLike(candidate) ? candidate : null
  } catch {
    return null
  }
}

export async function sendNodeNotifierNotification(
  notifier: NodeNotifierLike,
  platform: string,
  titleRaw: unknown,
  messageRaw: unknown,
  iconPathRaw?: unknown,
): Promise<boolean> {
  const options = buildNodeNotifierNotificationOptions(
    platform,
    titleRaw,
    messageRaw,
    iconPathRaw,
    runtimeProcessEnv(),
  )
  if (!options) return false

  return new Promise<boolean>((resolve) => {
    let settled = false
    let timer: ReturnType<typeof setTimeout> | null = null
    const finish = (value: boolean) => {
      if (settled) return
      settled = true
      if (timer) clearTimeout(timer)
      resolve(value)
    }
    timer = setTimeout(() => finish(false), NODE_NOTIFIER_CALLBACK_TIMEOUT_MS)
    unrefTimer(timer)

    try {
      notifier.notify(options, (err: unknown) => {
        if (err instanceof Error) {
          finish(false)
          return
        }
        if (typeof err === 'string' && err.trim()) {
          finish(false)
          return
        }
        finish(true)
      })
    } catch {
      finish(false)
    }
  })
}

function normalizeInteractionValue(value: unknown): string {
  return String(value ?? '')
    .trim()
    .toLowerCase()
}

function toSystemNotificationInteraction(
  responseRaw: unknown,
  metadataRaw: unknown,
  actions: readonly string[] = [],
): SystemNotificationInteraction | null {
  const response = normalizeInteractionValue(responseRaw)
  const metadata =
    metadataRaw && typeof metadataRaw === 'object'
      ? (metadataRaw as Record<string, unknown>)
      : {}
  if (!response) return null
  if (response === 'replied') {
    return {
      type: 'reply',
      value: String(metadata.activationValue ?? '').trim(),
    }
  }
  if (response === 'activate' || response === 'click') return { type: 'click' }
  if (response === 'timeout' || response === 'close') return { type: 'timeout' }

  const actionMap = new Map(
    actions.map(
      (action) => [normalizeInteractionValue(action), action] as const,
    ),
  )
  const matched = actionMap.get(response)
  if (matched) return { type: 'action', value: matched }

  return { type: 'action', value: String(responseRaw ?? '').trim() }
}

export function sendInteractiveNodeNotifierNotification(
  notifier: NodeNotifierLike,
  platform: string,
  request: InteractiveSystemNotificationRequest,
): boolean {
  const options = buildNodeNotifierNotificationOptions(
    platform,
    request.titleRaw,
    request.messageRaw,
    request.iconPathRaw,
    runtimeProcessEnv(),
  )
  if (!options) return false

  const interactiveOptions: Record<string, unknown> =
    platform === 'darwin'
      ? {
          ...options,
          ...(request.reply ? { reply: true } : {}),
          ...(request.actions && request.actions.length > 0
            ? { actions: [...request.actions] }
            : {}),
          ...(request.closeLabel ? { closeLabel: request.closeLabel } : {}),
          ...(typeof request.timeoutSeconds === 'number' &&
          Number.isFinite(request.timeoutSeconds) &&
          request.timeoutSeconds > 0
            ? { timeout: Math.max(1, Math.floor(request.timeoutSeconds)) }
            : {}),
        }
      : options

  try {
    notifier.notify(
      interactiveOptions,
      (err: unknown, response: unknown, metadata: unknown) => {
        if (err instanceof Error) return
        if (typeof err === 'string' && err.trim()) return
        const interaction = toSystemNotificationInteraction(
          response,
          metadata,
          request.actions,
        )
        if (interaction) request.onInteraction?.(interaction)
      },
    )
    return true
  } catch {
    return false
  }
}

export async function sendInteractiveSystemNotification(
  request: InteractiveSystemNotificationRequest,
): Promise<boolean> {
  const env = runtimeProcessEnv()
  if (env.DIMCODE_TEST === '1') return false
  if (String(env.DIMCODE_SYSTEM_NOTIFY ?? '').trim() === '0') return false

  const platform = runtimeProcessPlatform()
  if (!platform) return false

  const iconPath = resolveNotificationIconPath(env)
  const notifier = await loadNodeNotifier()
  if (!notifier)
    return sendSystemNotification(request.titleRaw, request.messageRaw)

  return sendInteractiveNodeNotifierNotification(notifier, platform, {
    ...request,
    iconPathRaw: request.iconPathRaw ?? iconPath,
  })
}

export async function sendSystemNotification(
  titleRaw: unknown,
  messageRaw: unknown,
): Promise<boolean> {
  const env = runtimeProcessEnv()
  if (env.DIMCODE_TEST === '1') return false
  if (String(env.DIMCODE_SYSTEM_NOTIFY ?? '').trim() === '0') return false

  const platform = runtimeProcessPlatform()
  if (!platform) return false

  const iconPath = resolveNotificationIconPath(env)
  const notifier = await loadNodeNotifier()
  if (notifier) {
    const ok = await sendNodeNotifierNotification(
      notifier,
      platform,
      titleRaw,
      messageRaw,
      iconPath,
    )
    if (ok) return true
  }

  const attempts = buildSystemNotificationAttempts(
    platform,
    titleRaw,
    messageRaw,
    iconPath,
  )
  for (const attempt of attempts) {
    // Sequential fallback: first successful notifier wins.
    // Some environments only provide one of powershell.exe/powershell/pwsh.
    const ok = await runNotifyAttempt(attempt)
    if (ok) return true
  }
  return false
}

function sanitizeNotificationIconPath(raw: unknown): string {
  const target = String(raw ?? '').trim()
  if (!target) return ''
  if (target.includes('\0')) return ''
  if (/[\r\n]/.test(target)) return ''
  if (!path.isAbsolute(target)) return ''
  if (!existsSync(target)) return ''
  return target
}

function toWindowsFileUri(iconPath: string): string {
  const normalized = String(iconPath ?? '').replace(/\\/g, '/')
  if (/^[A-Z]:\//i.test(normalized)) return `file:///${normalized}`
  return `file://${normalized}`
}

function resolveBundledNotificationIconPath(): string {
  const candidates = new Set<string>()

  try {
    const moduleDir = path.dirname(fileURLToPath(import.meta.url))
    candidates.add(path.resolve(moduleDir, '../icon.png'))
    candidates.add(path.resolve(moduleDir, '../../icon.png'))
    candidates.add(path.resolve(moduleDir, '../../../icon.png'))
  } catch {
    // ignore
  }

  const execPath = runtimeProcessExecPath()
  if (execPath) {
    const execDir = path.dirname(execPath)
    candidates.add(path.resolve(execDir, '../icon.png'))
    candidates.add(path.resolve(execDir, '../../icon.png'))
  }

  for (const candidate of candidates) {
    const resolved = sanitizeNotificationIconPath(candidate)
    if (resolved) return resolved
  }
  return ''
}

export function resolveNotificationIconPath(
  env: Record<string, string | undefined>,
): string {
  const fromEnv = sanitizeNotificationIconPath(env.DIMCODE_NOTIFICATION_ICON)
  if (fromEnv) return fromEnv

  const fromBundled = resolveBundledNotificationIconPath()
  if (fromBundled) return fromBundled

  return ''
}
