const openPackage = require('open')
const open = openPackage.default || openPackage
const http = require('http')
const { getConfig, setConfig, normalizeUrl } = require('./config')
const keytar = require('keytar')
const axios = require('axios')

const SERVICE_NAME = 'bild-cli'
const ACCOUNT_NAME = 'bildit-cms-token'

/** @param {string[]} names */
function firstNonEmptyTrimmedEnv(...names) {
  for (const name of names) {
    const v = process.env[name]
    if (v != null && String(v).trim() !== '') return String(v).trim()
  }
  return null
}

function getTokenFromEnv() {
  return (
    firstNonEmptyTrimmedEnv('BILD_TOKEN', 'BILD_FIREBASE_TOKEN') ||
    firstNonEmptyTrimmedEnv('CMS_TOKEN', 'FIREBASE_TOKEN') ||
    null
  )
}

/**
 * Strip accidental wrappers so callers do not send
 * `Authorization: Bearer Bearer <jwt>` (common when copying from curl, DevTools, or docs).
 * @param {string} token
 * @returns {string}
 */
function normalizeIdTokenForBearerHeader(token) {
  if (!token || typeof token !== 'string') return token
  let t = token.trim()

  const stripMatchingQuotes = (s) => {
    if (
      (s.startsWith('"') && s.endsWith('"')) ||
      (s.startsWith("'") && s.endsWith("'"))
    ) {
      return s.slice(1, -1).trim()
    }
    return s
  }

  t = stripMatchingQuotes(t)
  t = t.replace(/^authorization\s*:\s*/i, '').trim()
  t = stripMatchingQuotes(t)

  for (let i = 0; i < 5 && /^bearer\s+/i.test(t); i++) {
    t = t.replace(/^bearer\s+/i, '').trim()
  }

  return t
}

function validateToken(token) {
  if (!token || typeof token !== 'string') {
    return false
  }
  
  const trimmedToken = token.trim()
  
  if (trimmedToken.length === 0) {
    return false
  }
  
  const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/
  return jwtRegex.test(trimmedToken)
}

async function getStoredToken() {
  try {
    return await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME)
  } catch (error) {
    return null
  }
}

async function storeToken(token) {
  try {
    await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, token)
    return true
  } catch (error) {
    console.error('Error storing token:', error.message)
    return false
  }
}

async function clearStoredToken() {
  try {
    const deleted = await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME)
    return deleted
  } catch (error) {
    console.error('Error clearing token:', error.message)
    return false
  }
}

async function getSSOConfig(cmsUrl, apiKey) {
  try {
    const normalizedUrl = normalizeUrl(cmsUrl)
    const response = await axios.get(`${normalizedUrl}/api/config/sso`, {
      params: apiKey ? { key: apiKey } : {},
      validateStatus: (status) => status < 500, // Don't throw on 4xx
    })
    
    if (response.status === 403 || response.status === 401) {
      // SSO config requires auth, which we don't have yet
      return null
    }
    
    if (response.status === 200) {
      return response.data
    }
    
    throw new Error(`Failed to get SSO config: ${response.status}`)
  } catch (error) {
    if (error.response && (error.response.status === 403 || error.response.status === 401)) {
      return null // Return null instead of throwing for auth errors
    }
    throw new Error(`Failed to get SSO config: ${error.message}`)
  }
}

async function exchangeCodeForToken(code, callbackUrl, ssoConfig) {
  // Try CMS token endpoint first
  if (ssoConfig && ssoConfig.cmsUrl) {
    try {
      const normalizedCmsUrl = normalizeUrl(ssoConfig.cmsUrl)
      const tokenUrl = `${normalizedCmsUrl}/api/token`
      const response = await axios.post(tokenUrl, {
        code,
        grant_type: 'authorization_code',
        redirect_uri: callbackUrl,
      })
      
      if (response.data && response.data.token) {
        return response.data.token
      }
      if (response.data && response.data.idToken) {
        return response.data.idToken
      }
      if (response.data && response.data.access_token) {
        return response.data.access_token
      }
    } catch (error) {
      // CMS token endpoint failed, try Auth0
    }
  }

  // Use Auth0 token endpoint
  if (ssoConfig && ssoConfig.domain && ssoConfig.clientId) {
    const auth0TokenUrl = `https://${ssoConfig.domain}/oauth/token`
    
    const tokenPayload = {
      grant_type: 'authorization_code',
      client_id: ssoConfig.clientId,
      code,
      redirect_uri: callbackUrl,
    }
    
    // Only include client_secret if it exists (for confidential clients)
    if (ssoConfig.clientSecret) {
      tokenPayload.client_secret = ssoConfig.clientSecret
    }
    
    try {
      const response = await axios.post(auth0TokenUrl, tokenPayload, {
        headers: {
          'Content-Type': 'application/json',
        },
      })

      if (response.data && response.data.id_token) {
        return response.data.id_token
      }
      if (response.data && response.data.access_token) {
        return response.data.access_token
      }
      throw new Error('No token in Auth0 response')
    } catch (error) {
      const errorMsg = error.response?.data?.error_description || error.response?.data?.error || error.message
      throw new Error(`Token exchange failed: ${errorMsg}`)
    }
  }

  throw new Error('Unable to exchange code for token: No Auth0 configuration found')
}

async function authenticate(cmsUrl, auth0Config = null) {
  // Normalize URL to remove trailing slashes
  const normalizedCmsUrl = normalizeUrl(cmsUrl)
  
  // Use provided Auth0 config or try to get from SSO endpoint
  let ssoConfig = auth0Config
  
  if (!ssoConfig) {
    try {
      ssoConfig = await getSSOConfig(normalizedCmsUrl, null)
      if (ssoConfig) {
        console.log('Retrieved SSO configuration from CMS')
      }
    } catch (error) {
      // SSO config requires auth, which we don't have yet
      ssoConfig = null
    }
  }
  
  // If no SSO config, prompt for Auth0 details
  if (!ssoConfig || !ssoConfig.domain || !ssoConfig.clientId) {
    // No SSO config available; proceed via CMS sign-in page.
  }

  return new Promise((resolve, reject) => {
    let serverPort
    let timeoutId
    let isResolved = false
    
    const closeServer = () => {
      if (timeoutId) {
        clearTimeout(timeoutId)
        timeoutId = null
      }
      // Close all connections if available (Node 18+)
      if (server.closeAllConnections && typeof server.closeAllConnections === 'function') {
        server.closeAllConnections()
      }
      server.close(() => {
        // Server fully closed
      })
    }
    
    const server = http.createServer(async (req, res) => {
      if (req.url.startsWith('/callback') || req.url.includes('callback')) {
        try {
          const url = new URL(req.url, `http://${req.headers.host}`)
          
          const code = url.searchParams.get('code')
          const error = url.searchParams.get('error')
          const errorDescription = url.searchParams.get('error_description')

          if (error) {
            res.writeHead(400, { 'Content-Type': 'text/html' })
            res.end(`<html><body><h1>Authentication Failed</h1><p>${error}: ${errorDescription || ''}</p></body></html>`, () => {
              closeServer()
              if (!isResolved) {
                isResolved = true
                reject(new Error(`Authentication failed: ${error} - ${errorDescription || ''}`))
              }
            })
            return
          }

          if (code) {
            // Exchange authorization code for token
            try {
              const callbackUrl = `http://localhost:${serverPort}/callback`
              const token = await exchangeCodeForToken(code, callbackUrl, { ...ssoConfig, cmsUrl: normalizedCmsUrl })
              res.writeHead(200, { 'Content-Type': 'text/html' })
              res.end('<html><body><h1>Authentication Successful</h1><p>You can close this window.</p></body></html>', () => {
                closeServer()
                if (!isResolved) {
                  isResolved = true
                  resolve(token)
                }
              })
              return
            } catch (tokenError) {
              console.error('Token exchange error:', tokenError)
              res.writeHead(500, { 'Content-Type': 'text/html' })
              res.end(`<html><body><h1>Token Exchange Failed</h1><p>${tokenError.message}</p><p>You may need to manually extract the token from the browser.</p></body></html>`, () => {
                closeServer()
                if (!isResolved) {
                  isResolved = true
                  reject(tokenError)
                }
              })
              return
            }
          }

          // Fallback: check for direct token in URL
          const token = url.searchParams.get('token') || url.searchParams.get('access_token') || url.searchParams.get('idToken') || url.searchParams.get('id_token')
          if (token) {
            res.writeHead(200, { 'Content-Type': 'text/html' })
            res.end('<html><body><h1>Authentication Successful</h1><p>You can close this window.</p></body></html>', () => {
              closeServer()
              if (!isResolved) {
                isResolved = true
                resolve(token)
              }
            })
            return
          }

          res.writeHead(400, { 'Content-Type': 'text/html' })
          res.end(`<html><body><h1>Authentication Failed</h1><p>No authorization code or token received</p><p>Received params: ${url.searchParams.toString()}</p><p>Please check the browser console or Network tab for the token.</p></body></html>`, () => {
            closeServer()
            if (!isResolved) {
              isResolved = true
              reject(new Error('No authorization code or token received. Params: ' + url.searchParams.toString()))
            }
          })
        } catch (err) {
          console.error('Error processing callback:', err)
          res.writeHead(500, { 'Content-Type': 'text/html' })
          res.end(`<html><body><h1>Error</h1><p>${err.message}</p></body></html>`, () => {
            closeServer()
            if (!isResolved) {
              isResolved = true
              reject(err)
            }
          })
        }
      } else {
        res.writeHead(404, { 'Content-Type': 'text/html' })
        res.end('<html><body><h1>Not Found</h1></body></html>')
      }
    })

    server.listen(0, '127.0.0.1', () => {
      serverPort = server.address().port
      const callbackUrl = `http://localhost:${serverPort}/callback`
      
      // Build Auth0 authorization URL if SSO config is available
      let authUrl
      if (ssoConfig && ssoConfig.domain && ssoConfig.clientId) {
        // Use Auth0 OAuth flow directly
        const auth0Domain = ssoConfig.domain
        const clientId = ssoConfig.clientId
        const auth0AuthUrl = `https://${auth0Domain}/authorize`
        const params = new URLSearchParams({
          response_type: 'code',
          client_id: clientId,
          redirect_uri: callbackUrl,
          scope: 'openid profile email',
          state: 'bild-cli-auth',
        })
        authUrl = `${auth0AuthUrl}?${params.toString()}`
      } else {
        // Use CMS signin page - it may handle Auth0 internally
        const signinParams = new URLSearchParams({ callback: callbackUrl, redirect_uri: callbackUrl })
        authUrl = `${normalizedCmsUrl}/signin?${signinParams.toString()}`
      }

      console.log('')
      console.log('Opening browser for authentication...')
      console.log('After signing in, wait for: "Authentication complete. This window can be closed."')
      console.log('')
      
      open(authUrl).catch((err) => {
        console.error('Failed to open browser:', err.message)
        console.log(`Please visit: ${authUrl}`)
      })
    })

    // Timeout after 2 minutes - give user time to sign in and extract token manually
    timeoutId = setTimeout(() => {
      if (server.listening && !isResolved) {
        closeServer()
        if (!isResolved) {
          isResolved = true
          reject(new Error('Authentication timeout. Please try again.'))
        }
      }
    }, 2 * 60 * 1000)
  })
}

async function authenticateWithFirebase(cmsUrl, email, password, apiKey) {
  // Normalize URL to remove trailing slashes
  const normalizedCmsUrl = normalizeUrl(cmsUrl)
  
  // Firebase Auth REST API endpoint
  // Try to get API key from CMS config or use provided one
  let firebaseApiKey = apiKey
  
  if (!firebaseApiKey) {
    // Try to get from CMS config endpoint
    try {
      const configResponse = await axios.get(`${normalizedCmsUrl}/api/config`, {
        validateStatus: (status) => status < 500,
      })
      if (configResponse.status === 200 && configResponse.data.firebaseApiKey) {
        firebaseApiKey = configResponse.data.firebaseApiKey
      }
    } catch (error) {
      // Config endpoint might not be available, continue without it
    }
  }
  
  if (!firebaseApiKey) {
    throw new Error(
      'API key is required for email/password sign-in. Provide --auth-api-key or ensure the CMS exposes it in config.',
    )
  }
  
  // Firebase Auth REST API endpoint
  const firebaseAuthUrl = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${firebaseApiKey}`
  
  try {
    const response = await axios.post(firebaseAuthUrl, {
      email,
      password,
      returnSecureToken: true,
    }, {
      headers: {
        'Content-Type': 'application/json',
      },
    })
    
    if (response.data && response.data.idToken) {
      return response.data.idToken
    }
    
    throw new Error('No ID token in authentication response')
  } catch (error) {
    if (error.response) {
      const errorData = error.response.data
      const errorMessage = errorData?.error?.message || errorData?.error || error.message
      throw new Error(`Email/password sign-in failed: ${errorMessage}`)
    }
    throw new Error(`Email/password sign-in failed: ${error.message}`)
  }
}

async function login(cmsUrl, auth0Config = null, firebaseAuth = null) {
  try {
    // Normalize URL to remove trailing slashes
    const normalizedCmsUrl = normalizeUrl(cmsUrl)
    
    let token
    
    // If Firebase auth credentials provided, use Firebase Auth
    if (firebaseAuth && firebaseAuth.email && firebaseAuth.password) {
      token = await authenticateWithFirebase(normalizedCmsUrl, firebaseAuth.email, firebaseAuth.password, firebaseAuth.apiKey)
    } else {
      // Otherwise use OAuth flow
      token = await authenticate(normalizedCmsUrl, auth0Config)
    }
    
    await storeToken(token)
    setConfig('cmsUrl', normalizedCmsUrl)
    console.log('Authentication successful!')
    return token
  } catch (error) {
    console.error('Authentication failed:', error.message)
    throw error
  }
}

async function getToken() {
  const envToken = getTokenFromEnv()
  if (envToken) {
    return normalizeIdTokenForBearerHeader(envToken)
  }

  const storedToken = await getStoredToken()
  if (storedToken) {
    return normalizeIdTokenForBearerHeader(storedToken)
  }

  const config = getConfig()
  if (config.cmsUrl) {
    console.log('No stored token found. Please run: bild login')
    console.log('Or set BILD_TOKEN or CMS_TOKEN environment variable.')
    throw new Error('Not authenticated. Run "bild login" first or set token via environment variable.')
  }

  throw new Error('No CMS URL configured. Run "bild login <url>" first.')
}

async function getTokenSource() {
  if (firstNonEmptyTrimmedEnv('BILD_TOKEN', 'BILD_FIREBASE_TOKEN')) {
    return 'BILD_TOKEN environment variable'
  }

  if (firstNonEmptyTrimmedEnv('CMS_TOKEN', 'FIREBASE_TOKEN')) {
    return 'CMS_TOKEN environment variable'
  }
  
  const storedToken = await getStoredToken()
  if (storedToken) {
    return 'keychain'
  }
  
  return 'none'
}

async function displayToken() {
  const envToken = getTokenFromEnv()
  
  if (envToken) {
    const source = await getTokenSource()
    const token = normalizeIdTokenForBearerHeader(envToken)
    const isValid = validateToken(token)
    return {
      token,
      source,
      isValid,
    }
  }
  
  const storedToken = await getStoredToken()
  if (storedToken) {
    const token = normalizeIdTokenForBearerHeader(storedToken)
    const isValid = validateToken(token)
    return {
      token,
      source: 'keychain',
      isValid,
    }
  }
  
  return {
    token: null,
    source: 'none',
    isValid: false,
  }
}

module.exports = {
  login,
  logout: clearStoredToken,
  getToken,
  getStoredToken,
  storeToken,
  clearStoredToken,
  getTokenFromEnv,
  validateToken,
  getTokenSource,
  displayToken,
  normalizeIdTokenForBearerHeader,
}
