import { RECOMMENDED_BLOSSOM_SERVERS } from '@/constants'
import { simplifyUrl } from '@/lib/url'
import { TDraftEvent, TMediaUploadServiceConfig } from '@/types'
import { BlossomClient } from 'blossom-client-sdk'
import { z } from 'zod'
import client from './client.service'
import storage from './local-storage.service'

type UploadOptions = {
  onProgress?: (progressPercent: number) => void
  signal?: AbortSignal
}

export const UPLOAD_ABORTED_ERROR_MSG = 'Upload aborted'

class MediaUploadService {
  static instance: MediaUploadService

  private serviceConfig: TMediaUploadServiceConfig = storage.getMediaUploadServiceConfig()
  private nip96ServiceUploadUrlMap = new Map<string, string | undefined>()
  private imetaTagMap = new Map<string, string[]>()

  constructor() {
    if (!MediaUploadService.instance) {
      MediaUploadService.instance = this
    }
    return MediaUploadService.instance
  }

  setServiceConfig(config: TMediaUploadServiceConfig) {
    this.serviceConfig = config
  }

  async upload(file: File, options?: UploadOptions) {
    let result: { url: string; tags: string[][] }
    if (this.serviceConfig.type === 'nip96') {
      result = await this.uploadByNip96(this.serviceConfig.service, file, options)
    } else {
      result = await this.uploadByBlossom(file, options)
    }

    if (result.tags.length > 0) {
      this.imetaTagMap.set(result.url, ['imeta', ...result.tags.map(([n, v]) => `${n} ${v}`)])
    }
    return result
  }

  private async uploadByBlossom(file: File, options?: UploadOptions) {
    const pubkey = client.pubkey
    const signer = async (draft: TDraftEvent) => {
      if (!client.signer) {
        throw new Error('You need to be logged in to upload media')
      }
      return client.signer.signEvent(draft)
    }
    if (!pubkey) {
      throw new Error('You need to be logged in to upload media')
    }

    if (options?.signal?.aborted) {
      throw new Error(UPLOAD_ABORTED_ERROR_MSG)
    }

    options?.onProgress?.(0)

    // Pseudo-progress: advance gradually until main upload completes
    let pseudoProgress = 1
    let pseudoTimer: number | undefined
    const startPseudoProgress = () => {
      if (pseudoTimer !== undefined) return
      pseudoTimer = window.setInterval(() => {
        // Cap pseudo progress to 90% until we get real completion
        pseudoProgress = Math.min(pseudoProgress + 3, 90)
        options?.onProgress?.(pseudoProgress)
        if (pseudoProgress >= 90) {
          stopPseudoProgress()
        }
      }, 300)
    }
    const stopPseudoProgress = () => {
      if (pseudoTimer !== undefined) {
        clearInterval(pseudoTimer)
        pseudoTimer = undefined
      }
    }
    startPseudoProgress()

    let servers = await client.fetchBlossomServerList(pubkey)
    // Add recommended servers as fallback
    const uniqueServers = new Set(servers)
    RECOMMENDED_BLOSSOM_SERVERS.forEach((s) => uniqueServers.add(s))
    servers = Array.from(uniqueServers)

    if (servers.length === 0) {
      throw new Error('No Blossom services available')
    }
    const [mainServer, ...mirrorServers] = servers

    const auth = await BlossomClient.createUploadAuth(signer, file, {
      message: 'Uploading media file'
    })

    // Try each server until one succeeds
    let blob: { url: string; sha256?: string; size?: number; type?: string; nip94?: string[][] } | undefined
    let lastError: Error | undefined
    const allServers = [mainServer, ...mirrorServers]

    // Upload with timeout using XMLHttpRequest (works better on mobile)
    const uploadWithXHR = (server: string): Promise<typeof blob> => {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest()
        const uploadUrl = server.replace(/\/$/, '') + '/upload'

        xhr.open('PUT', uploadUrl)
        xhr.setRequestHeader('Authorization', 'Nostr ' + btoa(JSON.stringify(auth)))
        xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream')
        xhr.responseType = 'json'
        xhr.timeout = 15000 // 15 second timeout per server

        const handleAbort = () => {
          xhr.abort()
          reject(new Error(UPLOAD_ABORTED_ERROR_MSG))
        }
        if (options?.signal) {
          if (options.signal.aborted) return handleAbort()
          options.signal.addEventListener('abort', handleAbort, { once: true })
        }

        xhr.ontimeout = () => reject(new Error('Upload timed out'))
        xhr.onerror = () => reject(new Error('Network error'))
        xhr.onload = () => {
          if (xhr.status >= 200 && xhr.status < 300) {
            const data = xhr.response
            if (data?.url) {
              resolve({
                url: data.url,
                sha256: data.sha256,
                size: data.size,
                type: data.type,
                nip94: data.nip94
              })
            } else {
              reject(new Error('No URL in response'))
            }
          } else {
            reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`))
          }
        }
        xhr.send(file)
      })
    }

    for (const server of allServers) {
      try {
        // Try XHR first (better mobile support)
        blob = await uploadWithXHR(server)
        break
      } catch (xhrErr) {
        console.error(`Blossom XHR upload failed for ${server}:`, xhrErr)
        // Fallback to SDK with timeout
        try {
          const sdkPromise = BlossomClient.uploadBlob(server, file, { auth })
          const timeoutPromise = new Promise<never>((_, reject) =>
            setTimeout(() => reject(new Error('SDK upload timed out')), 15000)
          )
          const sdkBlob = await Promise.race([sdkPromise, timeoutPromise])
          blob = {
            url: sdkBlob.url,
            sha256: sdkBlob.sha256,
            size: sdkBlob.size,
            type: sdkBlob.type,
            nip94: (sdkBlob as any).nip94
          }
          break
        } catch (err) {
          console.error(`Blossom SDK upload failed for ${server}:`, err)
          lastError = err instanceof Error ? err : new Error(String(err))
        }
      }
    }

    if (!blob) {
      throw lastError ?? new Error('All Blossom servers failed')
    }

    // Main upload finished
    stopPseudoProgress()
    options?.onProgress?.(80)

    // Mirror to other servers (best effort) - only if we have sha256
    if (blob.sha256) {
      const successServer = blob.url ? new URL(blob.url).origin : mainServer
      const otherServers = allServers.filter((s) => s !== successServer)
      if (otherServers.length > 0) {
        const blobDescriptor = {
          url: blob.url,
          sha256: blob.sha256,
          size: blob.size ?? 0,
          type: blob.type ?? file.type,
          uploaded: Date.now()
        }
        await Promise.allSettled(
          otherServers.map((server) => BlossomClient.mirrorBlob(server, blobDescriptor, { auth }))
        )
      }
    }

    let tags: string[][] = []
    const parseResult = z.array(z.array(z.string())).safeParse((blob as any).nip94 ?? [])
    if (parseResult.success) {
      tags = parseResult.data
    }

    options?.onProgress?.(100)
    return { url: blob.url, tags }
  }

  private async uploadByNip96(service: string, file: File, options?: UploadOptions) {
    if (options?.signal?.aborted) {
      throw new Error(UPLOAD_ABORTED_ERROR_MSG)
    }
    let uploadUrl = this.nip96ServiceUploadUrlMap.get(service)
    if (!uploadUrl) {
      const response = await fetch(`${service}/.well-known/nostr/nip96.json`)
      if (!response.ok) {
        throw new Error(
          `${simplifyUrl(service)} does not work, please try another service in your settings`
        )
      }
      const data = await response.json()
      uploadUrl = data?.api_url
      if (!uploadUrl) {
        throw new Error(
          `${simplifyUrl(service)} does not work, please try another service in your settings`
        )
      }
      this.nip96ServiceUploadUrlMap.set(service, uploadUrl)
    }

    if (options?.signal?.aborted) {
      throw new Error(UPLOAD_ABORTED_ERROR_MSG)
    }
    const formData = new FormData()
    formData.append('file', file)

    const auth = await client.signHttpAuth(uploadUrl, 'POST', 'Uploading media file')

    // Use XMLHttpRequest for upload progress support
    const result = await new Promise<{ url: string; tags: string[][] }>((resolve, reject) => {
      const xhr = new XMLHttpRequest()
      xhr.open('POST', uploadUrl as string)
      xhr.responseType = 'json'
      xhr.setRequestHeader('Authorization', auth)

      const handleAbort = () => {
        try {
          xhr.abort()
        } catch {
          // ignore
        }
        reject(new Error(UPLOAD_ABORTED_ERROR_MSG))
      }
      if (options?.signal) {
        if (options.signal.aborted) {
          return handleAbort()
        }
        options.signal.addEventListener('abort', handleAbort, { once: true })
      }

      xhr.upload.onprogress = (event) => {
        if (event.lengthComputable) {
          const percent = Math.round((event.loaded / event.total) * 100)
          options?.onProgress?.(percent)
        }
      }
      xhr.onerror = () => reject(new Error('Network error'))
      xhr.onload = () => {
        if (xhr.status >= 200 && xhr.status < 300) {
          const data = xhr.response
          try {
            const tags = z.array(z.array(z.string())).parse(data?.nip94_event?.tags ?? [])
            const url = tags.find(([tagName]: string[]) => tagName === 'url')?.[1]
            if (url) {
              resolve({ url, tags })
            } else {
              reject(new Error('No url found'))
            }
          } catch (e) {
            reject(e as Error)
          }
        } else {
          reject(new Error(xhr.status.toString() + ' ' + xhr.statusText))
        }
      }
      xhr.send(formData)
    })

    return result
  }

  getImetaTagByUrl(url: string) {
    return this.imetaTagMap.get(url)
  }
}

const instance = new MediaUploadService()
export default instance
