const fs = require('fs')
const path = require('path')
const FormData = require('form-data')
const axios = require('axios')
const { getToken } = require('./auth')
const { getConfig } = require('./config')
const { APIError, enhanceFirebaseAuthUserMessage, collectAxiosErrorMeta } = require('./errors')
const { uploadObjectViaFirebaseStorageRest, isFirebaseIdToken } = require('./firebase-storage-media')
const { resolveCmsApiBaseUrl, adminListHeaders } = require('./cms-admin-api')

function getUploadTimeoutMs() {
  const config = getConfig()
  const fromConfig = config.uploadTimeoutMs
  if (typeof fromConfig === 'number' && fromConfig > 0) {
    return fromConfig
  }
  const fromEnv = process.env.BILD_UPLOAD_TIMEOUT_MS
  if (fromEnv) {
    const n = parseInt(fromEnv, 10)
    if (!Number.isNaN(n) && n > 0) return n
  }
  return 180000
}

function mediaDebugEnabled() {
  return (
    process.env.BILD_MEDIA_DEBUG === '1' ||
    process.env.BILD_DEBUG === '1' ||
    process.env.DEBUG === 'bild:media'
  )
}

function logMedia(message) {
  if (mediaDebugEnabled()) {
    console.error(`[bild media] ${message}`)
  }
}

async function createAuthedCmsAxios() {
  const cmsUrl = resolveCmsApiBaseUrl()
  if (!cmsUrl) {
    throw new Error('CMS URL not configured. Run "bild login <url>" first.')
  }
  const token = await getToken()
  return axios.create({
    baseURL: cmsUrl,
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      ...adminListHeaders(),
    },
    validateStatus: (status) => status < 500,
  })
}

function responseSnippetFromAxiosError(err) {
  const data = err && err.response && err.response.data
  if (data == null) return null
  return String(typeof data === 'string' ? data : JSON.stringify(data)).slice(0, 240)
}

/**
 * Upload route compatibility layer across BILDIT CMS builds.
 * Runtime evidence from staging: multipart to `/api/media/upload` can 404 even though the API exists (JSON unauth => 401),
 * so we also try `POST /api/media` and `POST /api/upload` which commonly accept multipart in newer deployments.
 *
 * @param {string} cmsUrl
 * @param {string|undefined} appId
 * @returns {string[]}
 */
function buildMediaUploadUrlCandidates(cmsUrl, appId) {
  const out = []
  if (!cmsUrl) return out
  out.push(`${cmsUrl}/api/media`)
  out.push(`${cmsUrl}/api/upload`)
  out.push(`${cmsUrl}/api/media/upload`)
  if (appId) {
    out.push(`${cmsUrl}/api/apps/${appId}/files/upload`)
    out.push(`${cmsUrl}/api/apps/${appId}/media/upload`)
    out.push(`${cmsUrl}/api/apps/${appId}/images/upload`)
    out.push(`${cmsUrl}/api/apps/${appId}/upload`)
  }
  return out
}

/**
 * Axios onUploadProgress only tracks bytes *sent* to the server. At 100% the HTTP
 * request body is on the wire; the client still waits for headers/body of the response.
 */
async function uploadMediaFile(filePath, appId) {
  const cmsUrl = resolveCmsApiBaseUrl()
  const timeoutMs = getUploadTimeoutMs()

  if (!cmsUrl) {
    throw new Error('CMS URL not configured. Run "bild login <url>" first.')
  }

  if (!fs.existsSync(filePath)) {
    throw new Error(`File not found: ${filePath}`)
  }

  const ext = path.extname(filePath)
  const base = path.basename(filePath, ext)
  const fileStats = fs.statSync(filePath)
  const token = await getToken()

  if (appId && isFirebaseIdToken(token)) {
    try {
      const authedClient = await createAuthedCmsAxios()
      const ext0 = path.extname(filePath)
      const base0 = path.basename(filePath, ext0)
      const storedFileName = `${base0}-${Date.now()}${ext0}`
      const url = await uploadObjectViaFirebaseStorageRest({
        filePath,
        appId,
        authedClient,
        idToken: token,
        contentType: getContentType(filePath),
        storedFileName,
      })
      return url
    } catch (e) {
      const st = e && e.response && e.response.status
      if (st === 401 || st === 403) {
        throw e
      }
      // For other failures (missing bucket, wrong path, etc.) try legacy HTTP routes for older deployments.
    }
  }

  const candidates = []
  for (const uploadUrl of buildMediaUploadUrlCandidates(cmsUrl, appId)) {
    for (const fileField of ['file', 'files']) {
      candidates.push({ uploadUrl, fileField })
    }
  }
  const attemptRows = []
  let lastError = null

  try {
    for (const [attemptIndex, { uploadUrl, fileField }] of candidates.entries()) {
      let waitTimer = null
      try {
        // IMPORTANT: streams/FormData are one-shot in Node; recreate per attempt.
        const formData = new FormData()
        const fileStream = fs.createReadStream(filePath)
        const fileName = `${base}-${Date.now()}${ext}`
        formData.append(fileField, fileStream, {
          filename: fileName,
          contentType: getContentType(filePath),
          knownLength: fileStats.size,
        })
        if (appId) {
          formData.append('appId', appId)
        }

        const startedAt = Date.now()
        let bodySentAt = null
        let sawProgressTotal = false

        let multipartContentLength = null
        try {
          multipartContentLength = formData.getLengthSync()
        } catch {
          multipartContentLength = null
        }

        logMedia(
          `Uploading ${path.basename(filePath)} (${fileStats.size} bytes) → ${uploadUrl} (field ${fileField}; timeout ${timeoutMs}ms)`
        )

        const response = await axios.post(uploadUrl, formData, {
          headers: {
            ...formData.getHeaders(),
            ...(multipartContentLength != null ? { 'Content-Length': String(multipartContentLength) } : {}),
            Authorization: `Bearer ${token}`,
          },
          maxContentLength: Infinity,
          maxBodyLength: Infinity,
          timeout: timeoutMs,
          onUploadProgress: (progressEvent) => {
            const { loaded, total } = progressEvent
            if (total) {
              sawProgressTotal = true
              const percentCompleted = Math.min(100, Math.round((loaded * 100) / total))
              process.stdout.write(`\rSending request body: ${percentCompleted}%`)
              if (loaded >= total && bodySentAt === null) {
                bodySentAt = Date.now()
                process.stdout.write('\n')
                process.stdout.write('Waiting for server response…')
                const waitStarted = Date.now()
                waitTimer = setInterval(() => {
                  const secs = ((Date.now() - waitStarted) / 1000).toFixed(0)
                  process.stdout.write(
                    `\rWaiting for server response… ${secs}s (max ${Math.round(timeoutMs / 1000)}s)`
                  )
                }, 500)
              }
            }
          },
        })

        if (waitTimer) {
          clearInterval(waitTimer)
          waitTimer = null
        }

        const totalMs = Date.now() - startedAt
        const bodyMs = bodySentAt != null ? bodySentAt - startedAt : null
        const serverMs = bodySentAt != null ? Date.now() - bodySentAt : null
        process.stdout.write('\n')
        logMedia(
          `POST ${uploadUrl} — file ${fileStats.size} bytes, ` +
            (bodyMs != null && serverMs != null
              ? `body sent in ${bodyMs}ms, response after ${(serverMs / 1000).toFixed(2)}s`
              : !sawProgressTotal
                ? `completed in ${totalMs}ms (no byte-level progress from client; cannot split send vs wait)`
                : `completed in ${totalMs}ms`) +
            `, status ${response.status}`
        )
        if (!mediaDebugEnabled() && bodyMs != null && serverMs != null && serverMs > 2000) {
          console.error(
            `[bild media] Server took ${(serverMs / 1000).toFixed(1)}s to respond after the file was sent; ` +
              `set BILD_MEDIA_DEBUG=1 for details.`
          )
        }

        return response.data.url || response.data
      } catch (err) {
        if (waitTimer) {
          clearInterval(waitTimer)
          waitTimer = null
        }
        process.stdout.write('\n')
        const isTimeout =
          err.code === 'ECONNABORTED' ||
          err.code === 'ETIMEDOUT' ||
          (typeof err.message === 'string' && err.message.includes('timeout'))
        const status = err.response && err.response.status
        const phase = isTimeout
          ? 'timed out waiting for HTTP response (file may already be on the server)'
          : err.response
            ? `HTTP ${err.response.status}`
            : err.code || err.message
        logMedia(`POST ${uploadUrl} (field ${fileField}) failed during: ${phase}`)
        if (isTimeout) {
          console.error(
            `[bild media] Timed out after ${timeoutMs}ms waiting for the server after the request was sent. ` +
              `Often the upload bytes finish first (see progress above); the delay is server-side. ` +
              `Try BILD_MEDIA_DEBUG=1 to log the URL and timings, or increase BILD_UPLOAD_TIMEOUT_MS.`
          )
        }

        const snippet = responseSnippetFromAxiosError(err)
        attemptRows.push({
          attemptIndex,
          uploadUrl,
          fileField,
          isTimeout: Boolean(isTimeout),
          code: err && err.code,
          status: status == null ? null : status,
          snippet,
        })
        lastError = err
        if (status === 401 || status === 403) {
          throw err
        }
        if (status === 413) {
          throw err
        }
        if (isTimeout) continue
        if (err.code === 'ECONNRESET' || err.message.includes('socket hang up')) continue
        if (status === 404) continue
        if (status === 400 || status === 415) continue
        if (status && status >= 500) continue
        continue
      }
    }
    
    // If we get here, all attempts failed
    if (!lastError) {
      throw new Error('Media upload failed: no attempts were made.')
    }
    throw lastError
  } catch (error) {
    process.stdout.write('\n') // Ensure new line after error
    if (error.response) {
      const { status, data } = error.response
      const baseMsg = data?.message || error.message || 'Media upload failed'
      throw new APIError(enhanceFirebaseAuthUserMessage(baseMsg, data), status, data, collectAxiosErrorMeta(error))
    }
    // Handle network errors (preserve .code so callers like resolveImageUrl can fall back to data URLs)
    if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || error.message.includes('socket hang up')) {
      const wrapped = new Error(
        `Media upload failed: Connection error. The upload endpoint may not be available or the file may be too large. Error: ${error.message}`
      )
      wrapped.code =
        error.code != null
          ? error.code
          : String(error.message).includes('socket hang up')
            ? 'ECONNRESET'
            : 'ETIMEDOUT'
      throw wrapped
    }
    throw error
  }
}

function getContentType(filePath) {
  const ext = path.extname(filePath).toLowerCase()
  const contentTypes = {
    '.jpg': 'image/jpeg',
    '.jpeg': 'image/jpeg',
    '.png': 'image/png',
    '.gif': 'image/gif',
    '.webp': 'image/webp',
    '.svg': 'image/svg+xml',
    '.bmp': 'image/bmp',
  }
  return contentTypes[ext] || 'application/octet-stream'
}

function isLocalFile(pathOrUrl) {
  // Check if it's a URL
  if (pathOrUrl.startsWith('http://') || pathOrUrl.startsWith('https://')) {
    return false
  }
  // Check if it's a data URL
  if (pathOrUrl.startsWith('data:')) {
    return false
  }
  // Otherwise assume it's a local file path
  return true
}

function fileToDataUrl(filePath) {
  const fileBuffer = fs.readFileSync(filePath)
  const base64 = fileBuffer.toString('base64')
  const contentType = getContentType(filePath)
  return `data:${contentType};base64,${base64}`
}

function getMaxEmbeddedImageDataUrlChars() {
  const config = getConfig()
  const fromConfig = config.maxEmbeddedImageDataUrlChars
  if (typeof fromConfig === 'number' && fromConfig > 0) {
    return fromConfig
  }
  const fromEnv = process.env.BILD_MAX_EMBEDDED_IMAGE_DATA_URL_CHARS
  if (fromEnv) {
    const n = parseInt(fromEnv, 10)
    if (!Number.isNaN(n) && n > 0) return n
  }
  // ~1.2M chars of base64+prefix — keeps JSON small enough for many API/proxy body limits
  return 1_200_000
}

/**
 * When CMS upload failed, we embed a data URL. Huge payloads break POST to the template library API /codelibs (4xx, sometimes 404 from proxies).
 * @param {string} resolvedPath
 * @returns {string}
 */
function embeddedDataUrlFromFileOrThrow(resolvedPath) {
  const dataUrl = fileToDataUrl(resolvedPath)
  const max = getMaxEmbeddedImageDataUrlChars()
  if (dataUrl.length > max) {
    throw new Error(
      `Embedded image is too large (${dataUrl.length} characters; max ${max}). ` +
        `After a failed media upload, the CLI can only store a data URL, which is too big for the API. ` +
        `Use a smaller image, \`--image\` with an https:// URL, or fix the media upload endpoint. ` +
        `To raise the cap: BILD_MAX_EMBEDDED_IMAGE_DATA_URL_CHARS.`
    )
  }
  return dataUrl
}

/**
 * Normalize pasted paths: trim, strip wrapping quotes, remove shell `\ ` before spaces
 * (including Unicode whitespace like U+202F used in macOS screenshot names).
 * @param {string} input
 */
function sanitizePathInput(input) {
  let s = String(input).trim()
  // Pasted from JSON/shell: "/path/to/file.png"
  if (
    (s.startsWith('"') && s.endsWith('"') && s.length >= 2) ||
    (s.startsWith("'") && s.endsWith("'") && s.length >= 2)
  ) {
    s = s.slice(1, -1).trim()
  }
  // `open\ file.png` — backslash before any Unicode whitespace (\s includes U+202F)
  return s.replace(/\\(?=\s)/gu, '')
}

/**
 * macOS screenshots often use a narrow no-break space (U+202F) before AM/PM; users paste a normal space.
 * @param {string} base
 * @returns {string[]}
 */
function expandBasenameVariants(base) {
  const out = new Set([base])
  out.add(base.replace(/\u202f/g, ' ').replace(/\u00a0/g, ' '))
  const narrowBeforeAmPm = base.replace(
    /\b(\d{1,2}\.\d{2}\.\d{2})\s+(AM|PM)\b/i,
    (_, t, ap) => `${t}\u202f${ap}`
  )
  out.add(narrowBeforeAmPm)
  return [...out]
}

/**
 * Resolve a local image path, including macOS screenshot quirks (U+202F before AM/PM).
 * @param {string} input
 * @returns {string|null} Absolute path to an existing file, or null
 */
function resolveLocalImagePath(input) {
  const trimmed = sanitizePathInput(input)
  if (!trimmed) return null

  const resolved = path.resolve(trimmed)
  const dir = path.dirname(resolved)
  const base = path.basename(resolved)

  const candidates = []
  const seen = new Set()
  const add = (p) => {
    if (!seen.has(p)) {
      seen.add(p)
      candidates.push(p)
    }
  }
  add(resolved)
  for (const b of expandBasenameVariants(base)) {
    add(path.join(dir, b))
  }

  for (const p of candidates) {
    try {
      if (fs.existsSync(p) && fs.statSync(p).isFile()) {
        return p
      }
    } catch {
      // ignore
    }
  }
  return null
}

function normalizeUploadedMediaUrl(data) {
  if (data == null) return null
  if (typeof data === 'string') return data
  if (typeof data === 'object' && typeof data.url === 'string') return data.url
  return data
}

async function resolveImageUrl(imagePathOrUrl, appId, options = {}) {
  // If it's already a URL, return it
  if (!isLocalFile(imagePathOrUrl)) {
    return imagePathOrUrl
  }

  const resolvedPath = resolveLocalImagePath(imagePathOrUrl)
  if (!resolvedPath) {
    const attempted = path.resolve(sanitizePathInput(imagePathOrUrl))
    throw new Error(`Image file not found: ${attempted}`)
  }

  // Prefer CMS upload so the template stores a normal storage URL (not a huge data: URL).
  if (appId) {
    try {
      const uploaded = await uploadMediaFile(resolvedPath, appId)
      const url = normalizeUploadedMediaUrl(uploaded)
      if (typeof url === 'string' && (url.startsWith('http://') || url.startsWith('https://'))) {
        return url
      }
      if (typeof url === 'string' && url.length > 0) {
        return url
      }
      console.warn(
        'Media upload response did not include a usable URL; using embedded data URL instead.'
      )
      return embeddedDataUrlFromFileOrThrow(resolvedPath)
    } catch (err) {
      const status = err.response?.status
      const isAxiosTimeout =
        err.code === 'ECONNABORTED' ||
        err.code === 'ETIMEDOUT' ||
        (typeof err.message === 'string' && err.message.includes('timeout'))
      const errMsg = typeof err.message === 'string' ? err.message : ''
      // Match wrapped upload errors and raw axios/Node client failures (code may be missing on some "socket hang up" errors)
      const isConnectionUploadFailure =
        err.code === 'ECONNRESET' ||
        err.code === 'EPIPE' ||
        errMsg.includes('socket hang up') ||
        errMsg.startsWith('Media upload failed: Connection error')
      const fallback =
        options.fallbackToDataUrl === true ||
        status === 404 ||
        isConnectionUploadFailure ||
        isAxiosTimeout
      if (fallback) {
        console.warn(
          `Media upload failed (${err.message || 'unknown error'}); using embedded data URL instead.`
        )
        return embeddedDataUrlFromFileOrThrow(resolvedPath)
      }
      throw err
    }
  }

  return embeddedDataUrlFromFileOrThrow(resolvedPath)
}

module.exports = {
  uploadMediaFile,
  resolveImageUrl,
  resolveLocalImagePath,
  isLocalFile,
  getContentType,
}

