import LABELS from './labels.js'
const { preactHooks } = globalThis.ami
const { useCallback, useEffect, useMemo, useRef, useState } = preactHooks

// Access token.
const AUTH_TOKEN_URL = '/bin/shell/imagegen'

// Backend service paths and configuration.
const ENDPOINTS = {
  AUTH: 'auth',
  GENERATE: 'images/generate-async',
  LIST: 'list',
  QUOTA_USAGE: 'quota-usage',
  USER_STATUS: 'user-status',
  ADMIN: 'admin'
}
const REQUEST_RETRY_COUNT = 1
const REQUEST_RETRY_INTERVAL = 1000
const REQUEST_TIMEOUT = 60_000
const STATUS_POLL_INTERVAL = 2000
const STATUS_POLL_ATTEMPTS = 60

// Component status.
export const STATUS = {
  LOADING: 1,
  READY: 2,
  PROCESSING: 3,
  ERROR: 4
}

// Prompt and load more status.
export const PROMPT_STATUS = {
  PENDING: 'pending',
  RUNNING: 'running',
  SUCCEEDED: 'succeeded',
  CANCELLED: 'canceled',
  FAILED: 'failed'
}

export const LOAD_MORE_STATUS = {
  NONE: 1,
  IN_PROGRESS: 2,
  ACTIVE: 3
}

// Model version.
const MODEL_VERSION = { standard: 'image4_standard', custom: 'image4_custom' }

// HTTP status codes. Note unlike the HTTP error names, we use the following semantics in the codebase:
// 401 - unauthenticated (not logged in), 403 - unauthorized (logged in but no access).
const HTTP_BAD_REQUEST = 400
const HTTP_UNAUTHORIZED = 401
const HTTP_FORBIDDEN = 403

// Simple helper moved 2 levels up from statusPoll() to satisfy Unicorn.
const isPendingStatus = status => !status || [PROMPT_STATUS.PENDING, PROMPT_STATUS.RUNNING].includes(status)

// Request error with user messaging (alert box).
class RequestError extends Error {
  constructor (message, { _response, _status, ...options }) {
    super(message, options)

    this._status = _status || _response?.status
    this._response = _response
  }

  async getUserMessage (display = true) {
    let userMessage

    // For generic request error, use the error message from the response body if available.
    if (this._response && (this._status === HTTP_BAD_REQUEST) && this._response.headers.get('Content-Type')?.includes('application/json')) {
      try {
        userMessage = (await this._response.json()).error
      } catch {
        // Ignore
      }
    }

    // Fall back to the default mapping.
    if (!userMessage) {
      const errorMap = {
        [HTTP_UNAUTHORIZED]: 'ERROR_LOGGED_OFF',
        [HTTP_FORBIDDEN]: 'ERROR_UNAUTHORISED'
      }
      const labelKey = errorMap[this._status] || 'ERROR'
      userMessage = LABELS[labelKey]
    }

    // Display the user message.
    display && alert(userMessage)

    return userMessage
  }
}

// Main hook to handle image generation requests and status polling. Not an isolated implementation, affects the main component state.
export default function useApi ({ apiUrl, isAnonymous, setState }) {
  const [prompts, setPrompts] = useState([])
  const [quotaUsage, setQuotaUsage] = useState({ total: null, usage: null })

  // Currently processed auth token promise, to avoid concurrent calls.
  const authPromiseRef = useRef(null)
  // Current auth token cached value.
  const authTokenRef = useRef(null)
  // Current prompt list for always fresh data in async functions - updated on each change. Null indicates not initialized yet.
  const promptsRef = useRef(null)
  // Counter to generate new prompt artificial IDs.
  const newPromptCounterRef = useRef(0)
  // Reference to the status polling handler to allow cleanup.
  const statusPollHandlerRef = useRef(null)

  useEffect(() => () => {
    statusPollHandlerRef.current && clearTimeout(statusPollHandlerRef.current)
  }, [])

  useEffect(() => {
    if (promptsRef.current !== null) {
      promptsRef.current = prompts
    }
  }, [prompts])

  // Decorate raw prompt with predefined custom fields.
  const parsePrompt = useCallback(prompt => ({
    ...prompt,
    images: prompt.result?.outputs?.map?.(output => {
      if (!output?.image?.url) {
        return null
      }
      const url = `${apiUrl}${output?.image?.url}`
      return {
        url,
        downloadUrl: url && `${url}${url.includes('?') ? '&' : '?'}download`,
        seed: output?.seed
      }
    }).filter(Boolean) || [],
    initDateTime: prompt.initTime && new Date(prompt.initTime)
  }), [apiUrl])

  // Auth token fetcher: returns true (for anonymous calls) or the access token on success, throws an exception on failure.
  const authenticate = useCallback(async () => {
    // Anonymous access in non-AEM environments.
    if (isAnonymous) {
      return true
    }

    if (authTokenRef.current) {
      return authTokenRef.current
    }

    // Get a new auth token.
    const res = await fetch(AUTH_TOKEN_URL, {
      signal: AbortSignal.timeout(REQUEST_TIMEOUT),
      method: 'GET',
      headers: {
        Accept: 'application/json'
      }
    })

    if (res.ok) {
      authTokenRef.current = (await res.json())?.token
    } else {
      // Always assume an authentication error, even if 404 (means the user logged out).
      throw new RequestError(`Get token: server responded with ${res.status}`, { _status: HTTP_UNAUTHORIZED })
    }

    return authTokenRef.current
  }, [isAnonymous])

  // Generic request sender with retry and auth handling.
  const sendRequest = useCallback(async (endpoint, { body, headers, ...props } = {}) => {
    let res
    for (let i = 0; i < REQUEST_RETRY_COUNT + 1; i++) {
      // Ensure there is always a single call to the auth services.
      if (!authPromiseRef.current) {
        authPromiseRef.current = authenticate()
      }
      // Authorization throws an error if failed.
      let token
      try {
        token = await authPromiseRef.current
      } finally {
        authPromiseRef.current = null
      }

      // Perform the main request.
      try {
        res = await fetch(`${apiUrl}${endpoint}`, {
          signal: AbortSignal.timeout(REQUEST_TIMEOUT),
          method: 'get',
          ...!isAnonymous && { credentials: 'include' },
          ...props,
          headers: {
            ...body && { 'Content-Type': 'application/json' },
            ...!isAnonymous && { Authorization: `Bearer ${token}` },
            ...headers
          },
          ...body && { body: JSON.stringify(body) }
        })
      } catch (error) {
        throw new RequestError(`Request failed: ${error.message}`, { cause: error })
      }

      if (res.ok) {
        break
      } else if ((i === REQUEST_RETRY_COUNT) || (res.status === HTTP_FORBIDDEN)) { // No retries for authorization errors.
        throw new RequestError(`Server responded with ${res.status}`, { _response: res })
      }
      if (res.status === HTTP_UNAUTHORIZED) {
        authTokenRef.current = null // Clear token on auth error to force refresh.
      }
      await new Promise(resolve => setTimeout(resolve, REQUEST_RETRY_INTERVAL)) // Wait before retrying.
    }

    return res
  }, [apiUrl, authenticate, isAnonymous])

  // Load prompt list (initial or load more).
  const loadPrompts = async () => {
    const isInitial = promptsRef.current === null
    promptsRef.current = prompts
    if (!isInitial) {
      setState(prev => ({
        ...prev,
        loadMore: LOAD_MORE_STATUS.IN_PROGRESS
      }))
    }

    let listJson
    let res
    try {
      // Pass lastId only for the subsequent calls (load more).
      const lastPromptWithId = isInitial ? undefined : prompts.findLast?.(p => p.id)
      const query = lastPromptWithId ? `?lastId=${encodeURIComponent(lastPromptWithId.id)}` : ''

      res = await sendRequest(`${ENDPOINTS.LIST}${query}`)
      listJson = await res.json()
      listJson.prompts = listJson.prompts?.map(parsePrompt)
    } catch (error) {
      console.error('Error fetching prompts:', error)
      if (isInitial) {
        // For the initial call, set the component state for a global error.
        const statusMessage = await error.getUserMessage?.(false)
        setState(prev => ({ ...prev, status: STATUS.ERROR, statusMessage }))
      } else {
        // Otherwise show the regular error message and display load more button again to allow retry.
        setState(prev => ({ ...prev, loadMore: LOAD_MORE_STATUS.ACTIVE }))
        await error.getUserMessage?.()
      }
      return
    }
    setPrompts(prev => [...prev, ...(listJson.prompts || [])])
    setState(prev => ({
      ...prev,
      ...prev.status === STATUS.LOADING && { status: STATUS.READY },
      loadMore: listJson.hasMore ? LOAD_MORE_STATUS.ACTIVE : LOAD_MORE_STATUS.NONE,
      ...!isInitial && { lastLoadPromptId: listJson.prompts[0]?.id }
    }))
    await updateQuotaUsage()
  }

  // Send new prompt request.
  const submitPrompt = useCallback(async (formData, onError) => {
    // Setting artificial newItemId to identify the prompt item while updating.
    let prompt = { newItemId: newPromptCounterRef.current++, text: formData.prompt, status: PROMPT_STATUS.PENDING, numVariations: formData.numVariations }
    setState(prev => ({ ...prev, status: STATUS.PROCESSING }))
    setPrompts(currPrompts => [prompt, ...currPrompts])

    let res
    try {
      res = await sendRequest(ENDPOINTS.GENERATE, {
        method: 'post',
        headers: {
          'x-model-version': MODEL_VERSION[formData.customModelId?.length ? 'custom' : 'standard']
        },
        body: formData
      })
      const { id, jobId, cancelUrl, statusUrl, initTime } = await res.json()
      prompt = parsePrompt({ ...prompt, id, jobId, cancelUrl, statusUrl, initTime, status: PROMPT_STATUS.RUNNING })
    } catch (error) {
      console.error('Error submitting image generation prompt:', error)
      await error.getUserMessage?.()
      prompt = { ...prompt, status: PROMPT_STATUS.FAILED }
      onError?.()
    }

    setPrompts(currPrompts => {
      const promptIndex = currPrompts.findIndex(p => p.newItemId === prompt.newItemId)
      return [
        ...currPrompts.slice(0, promptIndex),
        { ...currPrompts[promptIndex], ...prompt },
        ...currPrompts.slice(promptIndex + 1)
      ]
    })
    setState(prev => ({ ...prev, status: STATUS.READY }))
  }, [parsePrompt, sendRequest])

  // Polling function fired on prompt list change. If not all prompts are ready, the polling is continued after the interval.
  const statusPoll = async (attempt = 1) => {
    // Clear potential stale handler.
    clearTimeout(statusPollHandlerRef.current)

    let hasPendingPrompts = false
    for (const prompt of promptsRef.current || []) {
      if (!prompt?.statusUrl) {
        // Not ready yet - just initiated.
        hasPendingPrompts = true
        continue
      }
      if (!isPendingStatus(prompt.status) || !prompt.jobId) {
        continue
      }

      let resJson
      try {
        const res = await sendRequest(prompt.statusUrl)
        if (!res.ok) {
          throw new Error(`Server responded with ${res.status}`)
        }
        resJson = await res.json()
      } catch (error) {
        console.error('Error polling prompt status:', error)
        // No retry, just ignore to avoid overloading the server.
        continue
      }
      const statusMap = {
        succeeded: PROMPT_STATUS.SUCCEEDED,
        canceled: PROMPT_STATUS.CANCELLED,
        failed: PROMPT_STATUS.FAILED
      }
      const status = statusMap[resJson.status]
      if (isPendingStatus(status)) {
        hasPendingPrompts = true
        continue
      }

      setPrompts(currPrompts => {
        const promptIndex = currPrompts.findIndex(p => p.jobId === prompt.jobId)
        return [
          ...currPrompts.slice(0, promptIndex),
          parsePrompt({ ...prompt, status, result: resJson.result }),
          ...currPrompts.slice(promptIndex + 1)
        ]
      })
      await updateQuotaUsage()
    }
    if (hasPendingPrompts && (attempt < STATUS_POLL_ATTEMPTS)) {
      statusPollHandlerRef.current = setTimeout(() => statusPoll(attempt + 1), STATUS_POLL_INTERVAL)
    }
  }

  // Quota/usage updater.
  const updateQuotaUsage = async () => {
    try {
      const res = await sendRequest(ENDPOINTS.QUOTA_USAGE)
      if (!res.ok) {
        throw new Error(`Server responded with ${res.status}`)
      }
      const { total, usage } = await res.json()
      if (typeof total === 'number' && typeof usage === 'number') {
        setQuotaUsage({ total, usage })
      }
    } catch (error) {
      // Fail silently.
      console.error('Error getting quota/usage:', error)
    }
  }

  // Admin tools.
  const adminTools = useMemo(() => ({
    request: async (action, body) => {
      const isUserStatus = action === ENDPOINTS.USER_STATUS
      const endpoint = isUserStatus ? action : `${ENDPOINTS.ADMIN}/${action}`
      let res
      try {
        res = await sendRequest(endpoint, {
          method: action === 'user' ? 'post' : 'get',
          body
        })
      } catch (error) {
        console.error('Error requesting admin endpoint:', error)
        !isUserStatus && await error.getUserMessage?.()
        return null
      }

      return res.json()
    },

    getUrl: action => `${apiUrl}${ENDPOINTS.ADMIN}/${action}`
  }), [apiUrl, sendRequest])

  return [prompts, loadPrompts, statusPoll, submitPrompt, quotaUsage, adminTools]
}
