const fs = require('fs')
const axios = require('axios')
const { getConfig } = require('./config')
const { resolveCmsHttpBaseUrl } = require('./cms-http-base')

/**
 * @param {string} jwt
 * @returns {object|null}
 */
function decodeJwtPayloadUnverified(jwt) {
  if (typeof jwt !== 'string' || !jwt.includes('.')) return null
  const parts = jwt.split('.')
  if (parts.length < 2) return null
  const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/')
  const pad = b64.length % 4 === 0 ? '' : '='.repeat(4 - (b64.length % 4))
  try {
    const json = Buffer.from(b64 + pad, 'base64').toString('utf8')
    return JSON.parse(json)
  } catch {
    return null
  }
}

/**
 * @param {unknown} v
 * @returns {string|null}
 */
function asNonEmptyString(v) {
  if (v == null) return null
  if (typeof v === 'string' && v.trim().length > 0) return v.trim()
  return null
}

/**
 * @param {object} o
 * @param {string[]} keys
 * @returns {string|null}
 */
function pickStringDeep(o, keys) {
  if (!o || typeof o !== 'object') return null
  for (const k of keys) {
    if (Object.prototype.hasOwnProperty.call(o, k)) {
      const s = asNonEmptyString(o[k])
      if (s) return s
    }
  }
  for (const v of Object.values(o)) {
    if (v && typeof v === 'object') {
      const s = pickStringDeep(v, keys)
      if (s) return s
    }
  }
  return null
}

/**
 * @param {object} cfg
 * @returns {string|null}
 */
function pickStorageBucketFromCmsConfig(cfg) {
  if (!cfg || typeof cfg !== 'object') return null
  const direct = asNonEmptyString(cfg.storageBucket)
  if (direct) return direct
  const fromFirebase = pickStringDeep(cfg.firebase, ['storageBucket'])
  if (fromFirebase) return fromFirebase
  // Common alternate shapes
  return pickStringDeep(cfg, [
    'firebaseStorageBucket',
    'googleStorageBucket',
    'gcsBucket',
    'defaultBucket',
  ])
}

/**
 * @param {string} idToken
 * @returns {string|null} projectId (aud) when it looks like a Firebase ID token
 */
function projectIdFromFirebaseIdToken(idToken) {
  const p = decodeJwtPayloadUnverified(idToken)
  if (!p || typeof p !== 'object') return null
  const iss = asNonEmptyString(p.iss)
  const aud = asNonEmptyString(p.aud)
  if (!iss || !aud) return null
  if (!iss.startsWith('https://securetoken.google.com/')) return null
  return aud
}

/**
 * @param {string} idToken
 * @returns {boolean}
 */
function isFirebaseIdToken(idToken) {
  return projectIdFromFirebaseIdToken(idToken) != null
}

/**
 * @param {string} projectId
 * @returns {string}
 */
function defaultGcsBucketForFirebaseProjectId(projectId) {
  return `${projectId}.appspot.com`
}

/**
 * @param {string|undefined} tenantId
 * @param {string} appId
 * @param {string} fileName
 * @returns {string}
 */
function buildStorageObjectRelativePath(tenantId, appId, fileName) {
  const safeApp = String(appId).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')
  const safeName = String(fileName).replace(/\\/g, '/').replace(/^\/+/, '')
  const t = asNonEmptyString(tenantId)
  if (t) {
    const safeTenant = t.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')
    return `${safeTenant}/${safeApp}/${safeName}`
  }
  return `${safeApp}/${safeName}`
}

/**
 * @param {string} objectPath
 * @param {string} [downloadToken]
 * @returns {string}
 */
function buildFirebaseStorageDownloadUrl(bucket, objectPath, downloadToken) {
  const enc = encodeURIComponent(objectPath)
  const base = `https://firebasestorage.googleapis.com/v0/b/${encodeURIComponent(
    bucket
  )}/o/${enc}`
  if (downloadToken) {
    return `${base}?alt=media&token=${encodeURIComponent(downloadToken)}`
  }
  return `${base}?alt=media`
}

/**
 * @param {import('axios').AxiosInstance} client
 * @param {string} appId
 * @returns {Promise<string|null>}
 */
async function fetchTenantIdForApp(client, appId) {
  const candidates = [
    `/api/apps/${encodeURIComponent(appId)}`,
    `/api/app/${encodeURIComponent(appId)}`,
  ]
  for (const p of candidates) {
    try {
      const r = await client.get(p, { validateStatus: (s) => s === 200 || s === 404 })
      if (r.status !== 200 || !r.data || typeof r.data !== 'object') continue
      const t = pickStringDeep(r.data, ['tenantId', 'tenant_id', 'tenantID', 'tenant'])
      if (t) return t
    } catch {
      // ignore; try next candidate
    }
  }
  return null
}

/**
 * @returns {Promise<import('axios').AxiosInstance>}
 */
async function createAnonymousCmsClient() {
  const cmsUrl = resolveCmsHttpBaseUrl(getConfig().cmsUrl)
  if (!cmsUrl) throw new Error('CMS URL not configured')
  return axios.create({
    baseURL: cmsUrl,
    headers: { 'Content-Type': 'application/json' },
    validateStatus: (s) => s < 500,
  })
}

/**
 * @param {import('axios').AxiosInstance} authed
 * @returns {Promise<object|null>}
 */
async function fetchPublicishConfig(authed) {
  try {
    const r = await authed.get('/api/config', { validateStatus: (s) => s === 200 || s === 401 || s === 404 })
    if (r.status === 200 && r.data && typeof r.data === 'object') return r.data
  } catch {
    // ignore
  }
  try {
    const c = await createAnonymousCmsClient()
    const r2 = await c.get('/api/config', { validateStatus: (s) => s === 200 || s === 401 || s === 404 })
    if (r2.status === 200 && r2.data && typeof r2.data === 'object') return r2.data
  } catch {
    // ignore
  }
  return null
}

/**
 * Upload via Firebase Storage JSON API (same download URL family as the web client SDK getDownloadURL()).
 *
 * @param {object} args
 * @param {string} args.filePath
 * @param {string} args.appId
 * @param {import('axios').AxiosInstance} args.authedClient
 * @param {string} args.idToken
 * @param {string} args.contentType
 * @param {string} args.storedFileName
 * @returns {Promise<string>}
 */
async function uploadObjectViaFirebaseStorageRest(args) {
  const { filePath, appId, authedClient, idToken, contentType, storedFileName } = args
  const cfg = await fetchPublicishConfig(authedClient)
  let bucket =
    asNonEmptyString(process.env.BILD_STORAGE_BUCKET) || asNonEmptyString(process.env.BILD_FIREBASE_STORAGE_BUCKET)
  if (!bucket) bucket = pickStorageBucketFromCmsConfig(cfg)
  if (!bucket) {
    const pid = projectIdFromFirebaseIdToken(idToken)
    if (pid) bucket = defaultGcsBucketForFirebaseProjectId(pid)
  }
  if (!bucket) {
    const err = new Error(
      'Could not determine storage bucket (missing CMS config storage bucket and no project id in token)',
    )
    err.code = 'BILD_FBS_MISSING_BUCKET'
    throw err
  }

  const tenantId = (await fetchTenantIdForApp(authedClient, appId)) || pickStringDeep(cfg || {}, ['tenantId', 'tenant_id', 'tenant'])
  const objectPath = buildStorageObjectRelativePath(tenantId, appId, storedFileName)

  const url =
    `https://firebasestorage.googleapis.com/v0/b/${encodeURIComponent(bucket)}/o` +
    `?uploadType=media&name=${encodeURIComponent(objectPath)}`

  const fileStats = fs.statSync(filePath)
  const res = await axios.post(url, fs.createReadStream(filePath), {
    headers: {
      Authorization: `Bearer ${idToken}`,
      'Content-Type': contentType,
      'Content-Length': String(fileStats.size),
    },
    maxBodyLength: Infinity,
    maxContentLength: Infinity,
    // Allow large uploads; axios uses follow-redirects in Node; fine for this endpoint
    timeout: 180000,
    validateStatus: (s) => s >= 200 && s < 500,
  })

  if (res.status !== 200) {
    const msg =
      (res.data && (res.data.error && res.data.error.message)) ||
      (typeof res.data === 'string' ? res.data : JSON.stringify(res.data || {}))
    const err = new Error(`Storage upload failed: HTTP ${res.status} ${String(msg).slice(0, 300)}`)
    err.response = { status: res.status, data: res.data }
    throw err
  }

  let downloadToken = null
  const dt = res.data && res.data.downloadTokens
  if (typeof dt === 'string' && dt.trim().length > 0) {
    // Often a single token; if multiple, take the first.
    downloadToken = dt.split(',')[0].trim()
  } else if (Array.isArray(dt) && dt.length > 0) {
    downloadToken = asNonEmptyString(dt[0])
  }
  // Note: for images, the web SDK’s getDownloadURL is typically a tokenized URL. If the token is missing,
  // we still return `alt=media` (may be blocked by rules; callers can fall back).
  const outUrl = buildFirebaseStorageDownloadUrl(bucket, objectPath, downloadToken)
  return outUrl
}

module.exports = {
  decodeJwtPayloadUnverified,
  buildStorageObjectRelativePath,
  buildFirebaseStorageDownloadUrl,
  projectIdFromFirebaseIdToken,
  isFirebaseIdToken,
  defaultGcsBucketForFirebaseProjectId,
  pickStorageBucketFromCmsConfig,
  uploadObjectViaFirebaseStorageRest,
}
