const axios = require('axios')
const chalk = require('chalk')
const { getToken, getTokenSource } = require('./auth')
const { getConfig } = require('./config')
const { resolveCmsHttpBaseUrl } = require('./cms-http-base')
const { getCmsFunctionsBaseUrl, getCmsListAppsUserIdOverride, adminListHeaders, resolveCmsApiBaseUrl } = require('./cms-admin-api')
const { firebaseUidFromIdToken } = require('./id-token')
const { APIError, enhanceFirebaseAuthUserMessage, collectAxiosErrorMeta } = require('./errors')

/**
 * Best-effort extract of human-readable error text from CMS JSON bodies.
 * @param {unknown} data
 * @returns {string}
 */
function summarizeResponseMessage(data) {
  if (data == null) return ''
  if (typeof data === 'string') {
    const s = data.trim()
    return s.length > 320 ? `${s.slice(0, 317)}...` : s
  }
  const msg =
    data.message ||
    (typeof data.error === 'string' ? data.error : data.error?.message) ||
    (Array.isArray(data.errors) && data.errors[0] && data.errors[0].message)
  if (!msg) return ''
  const s = String(msg).trim()
  return s.length > 320 ? `${s.slice(0, 317)}...` : s
}

/**
 * Cloudflare (and similar) return 401/403 with text/html and a browser challenge page; axios cannot solve it.
 * @param {import('axios').AxiosResponse|undefined} res
 * @param {unknown} data
 * @returns {{ likely: boolean, reason: string|null }}
 */
function detectEdgeChallengeResponse(res, data) {
  if (!res) return { likely: false, reason: null }
  const h = res.headers || {}
  const ct = String(h['content-type'] || h['Content-Type'] || '').toLowerCase()
  const cfRay = h['cf-ray'] || h['CF-Ray']
  if (cfRay && ct.includes('text/html')) {
    return { likely: true, reason: 'cf-ray+text/html' }
  }
  if (typeof data === 'string') {
    const head = data.slice(0, 12_000)
    const low = head.toLowerCase()
    if (ct.includes('text/html') && /<!doctype\s+html/i.test(head)) {
      if (
        low.includes('just a moment') ||
        /__cf_chl|cf-browser-verification|challenge-platform|turnstile|checking your browser before accessing/i.test(
          low
        )
      ) {
        return { likely: true, reason: 'html+challenge-markers' }
      }
    }
  }
  return { likely: false, reason: null }
}

function buildEdgeChallengeUserMessage(status, res) {
  const ray = res && res.headers && (res.headers['cf-ray'] || res.headers['CF-Ray'])
  const rayHint = ray ? ` Cloudflare ray: ${String(ray).split(',')[0].trim()}.` : ''
  return (
    `HTTP ${status}: The BILDIT Visual Experience Engine detected a questionable component. ` +
    `Check the name, code, description, or file names. ` +
    `Things like "access" and "integrations" will trigger our WAF.${rayHint} ` +
    `Email support@bildit.co if you continue having trouble.`
  )
}

/**
 * Authenticated CMS JSON API client. Uses {@link resolveCmsApiBaseUrl} — when
 * `cmsFunctionsBaseUrl` / `BILD_CMS_FUNCTIONS_URL` is set, all routes (not only app list)
 * hit that origin (e.g. Cloud Functions) with optional `database-url` header.
 */
async function createClient() {
  const cmsUrl = resolveCmsApiBaseUrl()

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

  const token = await getToken()
  const client = axios.create({
    baseURL: cmsUrl,
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      ...adminListHeaders(),
    },
  })

  client.interceptors.request.use((config) => {
    if (process.env.BILD_DEBUG === '1') {
      let fullUrl
      try {
        fullUrl = axios.getUri(config)
      } catch {
        const base = String(config.baseURL || '').replace(/\/$/, '')
        const u = String(config.url || '')
        fullUrl = u.startsWith('http') ? u : `${base}${u.startsWith('/') ? '' : '/'}${u}`
      }
      const auth = config.headers && config.headers.Authorization
      let bearer = ''
      if (typeof auth === 'string') {
        bearer = /^Bearer\s+/i.test(auth) ? auth.replace(/^Bearer\s+/i, '').trim() : auth.trim()
      }
      if (bearer && !/^[\w-]+\.[\w-]+\.[\w-]*$/.test(bearer)) {
        console.error(
          chalk.yellow(
            '[bild debug] Bearer token does not look like a JWT — if requests fail, check BILD_TOKEN/CMS_TOKEN for a mistaken paste.',
          ),
        )
      }
      console.error(chalk.dim('[bild debug] CMS HTTP request (token is secret; do not paste logs publicly):'))
      console.error(chalk.dim(`[bild debug]   ${String(config.method || 'get').toUpperCase()} ${fullUrl}`))
      console.error(chalk.dim(`[bild debug]   Bearer ${bearer}`))
    }
    return config
  })

  client.interceptors.response.use(
    (response) => response,
    async (error) => {
      if (error.response) {
        const { status, data } = error.response

        if (status === 401 || status === 403) {
          const waf = detectEdgeChallengeResponse(error.response, data)
          if (waf.likely) {
            throw new APIError(
              enhanceFirebaseAuthUserMessage(buildEdgeChallengeUserMessage(status, error.response), data),
              status,
              data,
              collectAxiosErrorMeta(error)
            )
          }

          const serverDetail = summarizeResponseMessage(data)
          const detailSuffix = serverDetail ? ` Server response: ${serverDetail}` : ''

          // 403 is often RBAC / wrong app — not a stale CLI token (unless detectEdgeChallengeResponse already handled)
          if (status === 403) {
            const enhancedMessage =
              `HTTP 403 Forbidden.${detailSuffix} ` +
              `This usually means the signed-in user is not allowed to perform this action on this app ` +
              `(role, app membership, or route policy) — not necessarily an expired login. ` +
              `Verify default app ID and permissions in the CMS.`
            throw new APIError(enhanceFirebaseAuthUserMessage(enhancedMessage, data), status, data, collectAxiosErrorMeta(error))
          }

          const tokenSource = await getTokenSource()
          let enhancedMessage = data?.message || 'Authentication failed'

          if (tokenSource === 'none') {
            enhancedMessage =
              'Not authenticated. Run "bild login" or set BILD_TOKEN or CMS_TOKEN environment variable.' +
              detailSuffix
          } else if (tokenSource.includes('environment variable')) {
            enhancedMessage = `Authentication failed. Your token from ${tokenSource} may be invalid or expired. Try logging in again with "bild login".${detailSuffix}`
          } else {
            enhancedMessage = `Authentication failed. Your ${tokenSource} token may be invalid or expired. Try logging in again with "bild login" or use BILD_TOKEN or CMS_TOKEN environment variable.${detailSuffix}`
          }

          throw new APIError(enhanceFirebaseAuthUserMessage(enhancedMessage, data), status, data, collectAxiosErrorMeta(error))
        }

        const baseMsg = data?.message || error.message || 'API request failed'
        throw new APIError(enhanceFirebaseAuthUserMessage(baseMsg, data), status, data, collectAxiosErrorMeta(error))
      }
      throw error
    }
  )

  return client
}

async function createCodeLibrary(appId, data) {
  const client = await createClient()
  try {
    const response = await client.post(`/api/apps/${appId}/codelibs`, data)
    return response.data
  } catch (e) {
    if (e && e.name === 'APIError' && e.status === 404) {
      const img = data && data.image
      const bigData =
        typeof img === 'string' && img.startsWith('data:') && img.length > 400_000
      const hint = bigData
        ? ' Large embedded images (data URLs) can exceed proxy or API limits and fail with 404/413; use a smaller image, --image https://..., or fix media upload.'
        : ''
      throw new APIError(
        `${e.message} — POST template library API (/api/apps/${encodeURIComponent(appId)}/codelibs) returned 404 (not found). ` +
          `Check the app ID (\`bild library list\`), CMS base URL (\`bild login\`), and that the server exposes this route.${hint}`,
        404,
        e.data,
        e.requestUrl || e.responseHeaders
          ? { requestUrl: e.requestUrl, responseHeaders: e.responseHeaders }
          : undefined
      )
    }
    if (e && e.name === 'APIError' && e.status === 413) {
      throw new APIError(
        `${e.message} — Request body is too large (often a big data: image when media upload failed). ` +
          `Use a smaller image, \`--image\` with a hosted URL, or fix CMS media upload.`,
        413,
        e.data,
        e.requestUrl || e.responseHeaders
          ? { requestUrl: e.requestUrl, responseHeaders: e.responseHeaders }
          : undefined
      )
    }
    throw e
  }
}

async function updateCodeLibrary(appId, cid, data) {
  const client = await createClient()
  const response = await client.put(`/api/apps/${appId}/codelibs/${cid}`, data)
  return response.data
}

async function getCodeLibrary(appId, cid) {
  const client = await createClient()
  const response = await client.get(`/api/apps/${appId}/codelibs/${cid}`)
  return response.data
}

async function deleteCodeLibrary(appId, cid) {
  const client = await createClient()
  const response = await client.delete(`/api/apps/${appId}/codelibs/${cid}`)
  return response.data
}

async function listCodeLibraries(appId) {
  const client = await createClient()
  const response = await client.get(`/api/apps/${appId}/codelibs`)
  return response.data
}

async function listBanners(appId, params = {}) {
  const client = await createClient()
  const response = await client.get(`/api/apps/${appId}/banners`, { params })
  return response.data
}

async function updateBanner(appId, bid, data) {
  const client = await createClient()
  const response = await client.put(`/api/apps/${appId}/banners/${bid}`, data)
  return response.data
}

async function getSSOConfig(apiKey) {
  const config = getConfig()
  const cmsUrl = resolveCmsHttpBaseUrl(config.cmsUrl)
  const client = axios.create({ baseURL: cmsUrl })
  const response = await client.get('/api/config/sso', { params: { key: apiKey } })
  return response.data
}

async function getApps() {
  const client = await createClient()
  const fnBase = getCmsFunctionsBaseUrl()
  if (fnBase) {
    const token = await getToken()
    const uid = getCmsListAppsUserIdOverride() || firebaseUidFromIdToken(token)
    if (!uid) {
      throw new Error(
        'cmsFunctionsBaseUrl is set but the ID token has no sub (or user_id) claim. ' +
          'Set cmsListAppsUserId in ~/.bild.json or BILD_CMS_LIST_APPS_USER_ID.',
      )
    }
    const response = await client.get(`/api/admin/users/${encodeURIComponent(uid)}/apps`)
    return response.data
  }
  const response = await client.get('/api/apps')
  return response.data
}

/**
 * Current CMS user profile (uid, email, role, etc.).
 * @returns {Promise<object|null>}
 */
async function getUserProfile() {
  const client = await createClient()
  const response = await client.get('/api/user', {
    validateStatus: (status) => status === 200 || status === 404,
  })
  if (response.status !== 200) return null
  return response.data
}

async function getWebs() {
  const client = await createClient()
  const fnBase = getCmsFunctionsBaseUrl()
  if (fnBase) {
    const token = await getToken()
    const uid = getCmsListAppsUserIdOverride() || firebaseUidFromIdToken(token)
    if (!uid) {
      throw new Error(
        'cmsFunctionsBaseUrl is set but the ID token has no sub (or user_id) claim. ' +
          'Set cmsListAppsUserId in ~/.bild.json or BILD_CMS_LIST_APPS_USER_ID.',
      )
    }
    const res = await client.get(`/api/admin/users/${encodeURIComponent(uid)}/webs`, {
      validateStatus: (s) => s === 200 || s === 404,
    })
    if (res.status === 200) return res.data
  }
  const response = await client.get('/api/webs')
  return response.data
}

async function createVersion(appId, cid, versionData) {
  const client = await createClient()
  const response = await client.post(`/api/apps/${appId}/codelibs/${cid}/versions`, versionData)
  return response.data
}

async function transpileCode(code, codeType, appId) {
  const client = await createClient()
  const response = await client.post(`/api/apps/${appId}/transpile`, {
    code,
    codeType,
  })
  return response.data.compiled || response.data
}

/**
 * CMS “contents” collection (same model consumed by GET /remote-content).
 * @param {string} appId
 * @param {Record<string, unknown>} [params] — e.g. { device: 'web' }
 * @returns {Promise<unknown>}
 */
async function listContents(appId, params = {}) {
  const client = await createClient()
  const response = await client.get(`/api/apps/${appId}/contents`, { params })
  return response.data
}

/**
 * Parse new content id from POST response (body and/or Location header).
 * @param {import('axios').AxiosResponse} response
 * @returns {string|null}
 */
function extractNewContentId(response) {
  const data = response && response.data
  if (data && typeof data === 'object' && data.id != null) return String(data.id)
  if (typeof data === 'string') {
    const m = data.trim().match(/\/contents\/([^/?#]+)\/?$/)
    if (m) return decodeURIComponent(m[1])
  }
  const loc = response && (response.headers?.location || response.headers?.Location)
  if (typeof loc === 'string') {
    const m = loc.match(/\/contents\/([^/?#]+)\/?$/)
    if (m) return decodeURIComponent(m[1])
  }
  return null
}

/**
 * @param {string} appId
 * @param {Record<string, unknown>} data
 * @returns {Promise<{ id?: string|null } & Record<string, unknown>>}
 */
async function createContent(appId, data) {
  const client = await createClient()
  const response = await client.post(`/api/apps/${appId}/contents`, data)
  const id = extractNewContentId(response)
  const payload = response.data
  if (payload && typeof payload === 'object') return { ...payload, id: payload.id != null ? String(payload.id) : id }
  return { id }
}

/**
 * @param {string} appId
 * @param {string} cid
 * @param {Record<string, unknown>} data
 * @returns {Promise<unknown>}
 */
async function updateContent(appId, cid, data) {
  const client = await createClient()
  const response = await client.put(`/api/apps/${appId}/contents/${encodeURIComponent(cid)}`, data)
  return response.data
}

/**
 * @param {string} appId
 * @param {Record<string, unknown>} [params]
 * @returns {Promise<{ data?: unknown[], total?: number } | unknown[]>}
 */
async function listPages(appId, params = {}) {
  const client = await createClient()
  const response = await client.get(`/api/apps/${appId}/pages`, { params })
  return response.data
}

/**
 * @param {string} appId
 * @param {Record<string, unknown>} data
 * @returns {Promise<Record<string, unknown>>}
 */
async function createPage(appId, data) {
  const client = await createClient()
  const response = await client.post(`/api/apps/${appId}/pages`, data)
  return response.data
}

/**
 * @param {string} appId
 * @param {string} pid
 * @returns {Promise<Record<string, unknown>>}
 */
async function getPage(appId, pid) {
  const client = await createClient()
  const response = await client.get(`/api/apps/${appId}/pages/${encodeURIComponent(pid)}`)
  return response.data
}

/**
 * @param {string} appId
 * @param {string} pid
 * @param {Record<string, unknown>} data
 * @returns {Promise<unknown>}
 */
async function updatePage(appId, pid, data) {
  const client = await createClient()
  const response = await client.put(`/api/apps/${appId}/pages/${encodeURIComponent(pid)}`, data)
  return response.data
}

module.exports = {
  createCodeLibrary,
  updateCodeLibrary,
  getCodeLibrary,
  deleteCodeLibrary,
  listCodeLibraries,
  listBanners,
  updateBanner,
  getSSOConfig,
  getApps,
  getUserProfile,
  getWebs,
  createVersion,
  transpileCode,
  listContents,
  createContent,
  updateContent,
  extractNewContentId,
  listPages,
  createPage,
  getPage,
  updatePage,
  APIError,
  /** @internal tests & diagnostics */
  detectEdgeChallengeResponse,
  buildEdgeChallengeUserMessage,
}

