import { File as ExpoFile } from 'expo-file-system'
import { Video as CompressorVideo } from 'react-native-compressor'
import type { ProcessOptions, RawAsset } from './types'

const DEFAULT_VIDEO_TIMEOUT_MS = 120_000

export type CompressResult =
  | { ok: true; asset: RawAsset; compressed: boolean }
  | { ok: false; reason: 'too_large'; sizeMb: number; limitMb: number }
  | { ok: false; reason: 'failed' }
  | { ok: false; reason: 'aborted' }
  | { ok: false; reason: 'timeout' }

function fileSizeOrZero(uri: string): number {
  try {
    const f = new ExpoFile(uri)
    return f.exists ? f.size : 0
  } catch {
    return 0
  }
}

/** Compresses a video with react-native-compressor if the source is too big or
 *  not MP4. Honours AbortSignal and a hard timeout so a stuck compression
 *  doesn't hang the UI. */
export async function compressVideo(
  raw: RawAsset,
  opts: Pick<
    ProcessOptions,
    'maxVideoBytes' | 'forceMp4' | 'onVideoProgress' | 'signal' | 'videoTimeoutMs'
  >
): Promise<CompressResult> {
  if (opts.signal?.aborted) return { ok: false, reason: 'aborted' }

  const size = raw.size ?? fileSizeOrZero(raw.uri)
  const forceMp4 = opts.forceMp4 ?? true
  const needsTranscode = forceMp4 && raw.mimeType !== 'video/mp4'
  const needsShrink = size > opts.maxVideoBytes

  if (!needsTranscode && !needsShrink) {
    return { ok: true, asset: { ...raw, mimeType: 'video/mp4' }, compressed: false }
  }

  const timeoutMs = opts.videoTimeoutMs ?? DEFAULT_VIDEO_TIMEOUT_MS

  try {
    const compressedUri = await withTimeoutAndAbort(
      (internalSignal) =>
        CompressorVideo.compress(
          raw.uri,
          { compressionMethod: 'auto', minimumFileSizeForCompress: 0 },
          (progress) => {
            if (internalSignal.aborted) return
            opts.onVideoProgress?.(progress)
          }
        ),
      timeoutMs,
      opts.signal
    )

    const finalSize = fileSizeOrZero(compressedUri)
    if (finalSize > opts.maxVideoBytes) {
      return {
        ok: false,
        reason: 'too_large',
        sizeMb: finalSize / (1024 * 1024),
        limitMb: Math.floor(opts.maxVideoBytes / (1024 * 1024)),
      }
    }

    return {
      ok: true,
      asset: {
        ...raw,
        uri: compressedUri,
        mimeType: 'video/mp4',
        size: finalSize || undefined,
      },
      compressed: true,
    }
  } catch (err) {
    const reason = (err as Error | undefined)?.message ?? ''
    if (reason === 'aborted') return { ok: false, reason: 'aborted' }
    if (reason === 'timeout') return { ok: false, reason: 'timeout' }
    return { ok: false, reason: 'failed' }
  }
}

function withTimeoutAndAbort<T>(
  run: (signal: AbortSignal) => Promise<T>,
  timeoutMs: number,
  outer: AbortSignal | undefined
): Promise<T> {
  const ctrl = new AbortController()
  const onOuterAbort = () => ctrl.abort(new Error('aborted'))
  if (outer) {
    if (outer.aborted) ctrl.abort(new Error('aborted'))
    else outer.addEventListener('abort', onOuterAbort)
  }
  const timer = setTimeout(() => ctrl.abort(new Error('timeout')), timeoutMs)

  return new Promise<T>((resolve, reject) => {
    let settled = false
    ctrl.signal.addEventListener('abort', () => {
      if (settled) return
      settled = true
      clearTimeout(timer)
      // react-native-compressor exposes a module-level cancel; try to stop
      // the native job. Safe no-op if the API isn't present.
      try {
        const mod = CompressorVideo as unknown as { cancelCompression?: () => void }
        mod.cancelCompression?.()
      } catch {
        /* ignore */
      }
      const reason = (ctrl.signal.reason as Error | undefined)?.message ?? 'aborted'
      reject(new Error(reason))
    })
    run(ctrl.signal)
      .then((value) => {
        if (settled) return
        settled = true
        clearTimeout(timer)
        resolve(value)
      })
      .catch((err) => {
        if (settled) return
        settled = true
        clearTimeout(timer)
        reject(err)
      })
      .finally(() => {
        if (outer) outer.removeEventListener('abort', onOuterAbort)
      })
  })
}
