/**
 * ESLint rule to validate manifest.json configuration
 * @fileoverview Validates that manifest.json contains required scopes and permissions
 */

const fs = require('fs')
const path = require('path')

// Load the hook-scopes map
const hookScopesMap = require('../../generated-hook-maps/hook-scopes-map.json')

// Load the component-scopes map (may not exist yet)
let componentScopesMap = {}
try {
  componentScopesMap = require('../../generated-hook-maps/component-scopes-map.json')
} catch (err) {
  // Component scopes map doesn't exist yet, that's okay
}

// Module-level cache for manifest.json to avoid repeated file I/O
// ESLint reuses the same rule module across all files, so this cache
// persists for the entire lint run, dramatically improving performance
// Cache is invalidated if manifest.json modification time changes (for IDE support)
let manifestCache = null
let manifestPathCache = null
let manifestParseErrorCache = null
let manifestMtimeCache = null

// Hook to permission mappings
const hookPermissionsMap = {
  useImagePicker: ['CAMERA'],
}

// Extract domain from URL (returns domain in CSP format)
function extractDomain(url) {
  if (!url || typeof url !== 'string') return null

  try {
    // Handle relative URLs
    if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {
      return null
    }

    // Handle data: and blob: URLs
    if (url.startsWith('data:') || url.startsWith('blob:')) {
      return null
    }

    // Parse URL
    const urlObj = new URL(url)
    return urlObj.hostname
  } catch (err) {
    // If URL parsing fails, might be a relative path
    return null
  }
}

// Check if domain matches a pattern (supports wildcards)
function domainMatchesPattern(domain, pattern) {
  if (pattern === '*') return true
  if (pattern === domain) return true

  // Handle wildcard subdomains (*.example.com)
  if (pattern.startsWith('*.')) {
    const basePattern = pattern.slice(2)
    return domain === basePattern || domain.endsWith(`.${basePattern}`)
  }

  return false
}

module.exports = {
  meta: {
    type: 'problem',
    docs: {
      description:
        'Ensure manifest.json includes required scopes and permissions for Shop Minis features',
      category: 'Possible Errors',
      recommended: true,
    },
    fixable: 'code',
    messages: {
      missingScope:
        '{{source}} requires scope "{{scope}}" in src/manifest.json. Add "{{scope}}" to the "scopes" array.',
      missingPermission:
        '{{reason}} requires permission "{{permission}}" in src/manifest.json. Add "{{permission}}" to the "permissions" array.',
      missingTrustedDomain:
        '{{reason}} loads from "{{domain}}" which is not in trusted_domains. Add "{{domain}}" to the "trusted_domains" array in src/manifest.json.',
      manifestNotFound:
        'Cannot find src/manifest.json. Create a manifest file with required scopes/permissions/trusted_domains.',
      manifestInvalidJson:
        'Failed to parse src/manifest.json: {{error}}. Please fix the JSON syntax (common issues: trailing commas, missing quotes).',
    },
    schema: [],
  },

  create(context) {
    // eslint-disable-next-line @shopify/prefer-module-scope-constants
    const SDK_PACKAGE = '@shopify/shop-minis-react'
    let manifestPath = null
    let manifest = null
    let manifestParseError = null
    const usedHooks = new Set()
    const usedComponents = new Set()
    const requiredPermissions = new Set()
    const requiredDomains = new Set()
    const fixedIssues = new Set()

    // Check module-level cache first to avoid repeated file I/O
    if (manifestPathCache && fs.existsSync(manifestPathCache)) {
      // Check if manifest.json has been modified (for IDE integration)
      const currentMtime = fs.statSync(manifestPathCache).mtimeMs

      if (manifestMtimeCache === currentMtime) {
        // Cache is still valid - use it
        manifestPath = manifestPathCache
        manifest = manifestCache
        manifestParseError = manifestParseErrorCache
      } else {
        // File was modified - invalidate cache and reload
        manifestCache = null
        manifestParseErrorCache = null
        manifestMtimeCache = null
      }
    }

    // Load manifest if not cached or cache was invalidated
    if (!manifestPath) {
      // Cache miss - find and load manifest.json
      const filename = context.getFilename()
      if (filename && filename !== '<input>') {
        const dir = path.dirname(filename)
        // Look for src/manifest.json
        let currentDir = dir
        for (let i = 0; i < 5; i++) {
          const testPath = path.join(currentDir, 'src', 'manifest.json')
          if (fs.existsSync(testPath)) {
            manifestPath = testPath
            try {
              manifest = JSON.parse(fs.readFileSync(testPath, 'utf8'))
              // Store modification time for cache invalidation
              manifestMtimeCache = fs.statSync(testPath).mtimeMs
            } catch (err) {
              // Invalid JSON - save error for reporting
              manifestParseError = err.message
            }
            break
          }
          currentDir = path.join(currentDir, '..')
        }
      }

      // Update cache for subsequent files
      manifestPathCache = manifestPath
      manifestCache = manifest
      manifestParseErrorCache = manifestParseError
    }

    return {
      // Track imports from the SDK
      ImportDeclaration(node) {
        if (node.source.value === SDK_PACKAGE) {
          // eslint-disable-next-line @shopify/prefer-early-return
          node.specifiers.forEach(spec => {
            if (spec.type === 'ImportSpecifier') {
              const importedName = spec.imported.name

              // Check if it's a hook that requires scopes
              if (hookScopesMap[importedName]) {
                usedHooks.add(importedName)
              }

              // Check if it's a hook that requires permissions
              if (hookPermissionsMap[importedName]) {
                hookPermissionsMap[importedName].forEach(permission => {
                  requiredPermissions.add({
                    permission,
                    reason: `Hook "${importedName}"`,
                    node,
                  })
                })
              }

              // Check if it's a component that requires scopes
              // We need to check all possible paths where the component might be defined
              const possibleComponentPaths = Object.keys(
                componentScopesMap
              ).filter(componentPath => {
                // Extract the component name from the path (e.g., "commerce/add-to-cart" -> "AddToCartButton")
                // We'll check if the import name matches common component naming patterns
                const pathParts = componentPath.split('/')
                const fileName = pathParts[pathParts.length - 1]
                // Convert kebab-case or snake_case to PascalCase
                const componentName = fileName
                  .split(/[-_]/)
                  .map(part => part.charAt(0).toUpperCase() + part.slice(1))
                  .join('')
                return (
                  componentName === importedName || fileName === importedName
                )
              })

              if (possibleComponentPaths.length > 0) {
                possibleComponentPaths.forEach(componentPath => {
                  usedComponents.add({
                    path: componentPath,
                    name: importedName,
                    node,
                  })
                })
              }
            }
          })
        }
      },

      // Detect browser API usage for permissions
      MemberExpression(node) {
        const sourceCode = context.getSourceCode()
        const code = sourceCode.getText(node)

        // Check for camera/microphone API
        if (
          code.includes('navigator.mediaDevices.getUserMedia') ||
          code.includes('navigator.getUserMedia')
        ) {
          // Need to check the constraints to determine if it's camera or microphone
          // For now, we'll flag both as potentially needed
          requiredPermissions.add({
            permission: 'CAMERA',
            reason: 'getUserMedia API usage',
            node,
          })
          requiredPermissions.add({
            permission: 'MICROPHONE',
            reason: 'getUserMedia API usage',
            node,
          })
        }
      },

      // Detect network requests and event listeners
      CallExpression(node) {
        if (!node.arguments[0]) {
          return
        }

        const firstArg = node.arguments[0]

        // Check for fetch() calls
        if (node.callee.name === 'fetch' && firstArg.type === 'Literal') {
          const url = firstArg.value
          const domain = extractDomain(url)
          if (domain) {
            requiredDomains.add({
              domain,
              reason: 'fetch() call',
              node,
            })
          }
        }

        // Check for XMLHttpRequest.open()
        if (
          node.callee.type === 'MemberExpression' &&
          node.callee.property.name === 'open' &&
          node.arguments[1] &&
          node.arguments[1].type === 'Literal'
        ) {
          const url = node.arguments[1].value
          const domain = extractDomain(url)
          if (domain) {
            requiredDomains.add({
              domain,
              reason: 'XMLHttpRequest.open() call',
              node,
            })
          }
        }

        // Check for WebSocket
        if (node.callee.name === 'WebSocket' && firstArg.type === 'Literal') {
          const url = firstArg.value
          // WebSocket URLs use ws:// or wss://
          const domain = url.replace(/^wss?:\/\//, 'https://')
          const extracted = extractDomain(domain)
          if (extracted) {
            requiredDomains.add({
              domain: extracted,
              reason: 'WebSocket connection',
              node,
            })
          }
        }

        // Check for EventSource
        if (node.callee.name === 'EventSource' && firstArg.type === 'Literal') {
          const url = firstArg.value
          const domain = extractDomain(url)
          if (domain) {
            requiredDomains.add({
              domain,
              reason: 'EventSource connection',
              node,
            })
          }
        }

        // Check for navigator.sendBeacon()
        if (
          node.callee.type === 'MemberExpression' &&
          node.callee.property.name === 'sendBeacon' &&
          firstArg.type === 'Literal'
        ) {
          const url = firstArg.value
          const domain = extractDomain(url)
          if (domain) {
            requiredDomains.add({
              domain,
              reason: 'navigator.sendBeacon() call',
              node,
            })
          }
        }

        // Check for window.open()
        if (
          node.callee.type === 'MemberExpression' &&
          node.callee.property.name === 'open' &&
          node.callee.object.name === 'window' &&
          firstArg.type === 'Literal'
        ) {
          const url = firstArg.value
          const domain = extractDomain(url)
          if (domain) {
            requiredDomains.add({
              domain,
              reason: 'window.open() call',
              node,
            })
          }
        }

        // Check addEventListener for device motion/orientation
        if (
          node.callee.type === 'MemberExpression' &&
          node.callee.property.name === 'addEventListener' &&
          firstArg.type === 'Literal'
        ) {
          const eventName = firstArg.value
          if (
            eventName === 'deviceorientation' ||
            eventName === 'devicemotion'
          ) {
            requiredPermissions.add({
              permission: 'MOTION',
              reason: `${eventName} event listener`,
              node,
            })
          }
        }
      },

      // Check JSX attributes for external URLs
      JSXAttribute(node) {
        if (!node.value || node.value.type !== 'Literal') {
          return
        }

        const attrName = node.name.name
        const url = node.value.value
        const domain = extractDomain(url)

        if (!domain) {
          return
        }

        const elementName = node.parent.name.name

        // src attributes (img, video, audio, source, track, embed)
        // Exclude: script, iframe (not supported)
        if (attrName === 'src') {
          const supportedElements = [
            'img',
            'video',
            'audio',
            'source',
            'track',
            'embed',
            'Image',
            'VideoPlayer',
          ]
          if (supportedElements.includes(elementName)) {
            requiredDomains.add({
              domain,
              reason: `<${elementName}> src attribute`,
              node,
            })
          }
        }
        // poster attribute (video)
        else if (attrName === 'poster') {
          requiredDomains.add({
            domain,
            reason: `<${elementName}> poster attribute`,
            node,
          })
        }
        // data attribute (object)
        else if (attrName === 'data') {
          requiredDomains.add({
            domain,
            reason: `<${elementName}> data attribute`,
            node,
          })
        }
        // action attribute (form)
        else if (attrName === 'action') {
          requiredDomains.add({
            domain,
            reason: `<${elementName}> action attribute`,
            node,
          })
        }
      },

      // Detect DeviceOrientation/Motion events
      Identifier(node) {
        if (
          node.name === 'DeviceOrientationEvent' ||
          node.name === 'DeviceMotionEvent'
        ) {
          requiredPermissions.add({
            permission: 'MOTION',
            reason: `${node.name} usage`,
            node,
          })
        }
      },

      // Check everything at the end
      'Program:exit': function () {
        const issues = []

        // If manifest exists but has parse error, report it once
        if (
          manifestParseError &&
          (usedHooks.size > 0 ||
            usedComponents.size > 0 ||
            requiredPermissions.size > 0 ||
            requiredDomains.size > 0)
        ) {
          context.report({
            loc: {line: 1, column: 0},
            messageId: 'manifestInvalidJson',
            data: {error: manifestParseError},
          })
          return
        }

        // Check scopes for hooks
        usedHooks.forEach(hookName => {
          const requiredScopes = hookScopesMap[hookName]
          if (!requiredScopes || requiredScopes.length === 0) {
            return
          }

          if (!manifest) {
            issues.push({
              messageId: 'manifestNotFound',
              data: {hookName, scopes: requiredScopes.join(', ')},
            })
            return
          }

          const manifestScopes = manifest.scopes || []

          requiredScopes.forEach(requiredScope => {
            if (!manifestScopes.includes(requiredScope)) {
              issues.push({
                type: 'scope',
                scope: requiredScope,
                hookName,
              })
            }
          })
        })

        // Check scopes for components
        usedComponents.forEach(
          ({path: componentPath, name: componentName, node}) => {
            const componentData = componentScopesMap[componentPath]
            if (
              !componentData ||
              !componentData.scopes ||
              componentData.scopes.length === 0
            ) {
              return
            }

            const requiredScopes = componentData.scopes

            if (!manifest) {
              issues.push({
                messageId: 'manifestNotFound',
                data: {
                  hookName: componentName,
                  scopes: requiredScopes.join(', '),
                },
              })
              return
            }

            const manifestScopes = manifest.scopes || []

            requiredScopes.forEach(requiredScope => {
              if (!manifestScopes.includes(requiredScope)) {
                issues.push({
                  type: 'scope',
                  scope: requiredScope,
                  componentName,
                  componentPath,
                  node,
                })
              }
            })
          }
        )

        // Check permissions
        requiredPermissions.forEach(({permission, reason, node}) => {
          if (!manifest) {
            issues.push({
              messageId: 'manifestNotFound',
              data: {reason, permission},
            })
            return
          }

          const manifestPermissions = manifest.permissions || []

          if (!manifestPermissions.includes(permission)) {
            issues.push({
              type: 'permission',
              permission,
              reason,
              node,
            })
          }
        })

        // Check trusted domains
        requiredDomains.forEach(({domain, reason, node}) => {
          if (!manifest) {
            issues.push({
              messageId: 'manifestNotFound',
              data: {reason, domain},
            })
            return
          }

          const trustedDomains = manifest.trusted_domains || []

          // Check if domain is trusted (supports wildcards)
          const isTrusted = trustedDomains.some(pattern =>
            domainMatchesPattern(domain, pattern)
          )

          if (!isTrusted) {
            issues.push({
              type: 'domain',
              domain,
              reason,
              node,
            })
          }
        })

        // Report all issues
        issues.forEach(issue => {
          if (issue.messageId) {
            context.report({
              loc: {line: 1, column: 0},
              messageId: issue.messageId,
              data: issue.data,
            })
          } else if (issue.type === 'scope') {
            const issueKey = `scopes:${issue.scope}`
            const fixer = createManifestFixer('scopes', issue.scope)

            // Call fixer immediately to check if we can fix
            fixer()

            // Only report if not fixed
            if (!fixedIssues.has(issueKey)) {
              // Determine if this is from a hook or component
              const sourceName = issue.hookName || issue.componentName
              const sourceType = issue.hookName ? 'Hook' : 'Component'

              context.report({
                loc: {line: 1, column: 0},
                messageId: 'missingScope',
                data: {
                  source: `${sourceType} "${sourceName}"`,
                  scope: issue.scope,
                },
              })
            }
          } else if (issue.type === 'permission') {
            const issueKey = `permissions:${issue.permission}`
            const fixer = createManifestFixer('permissions', issue.permission)

            fixer()

            if (!fixedIssues.has(issueKey)) {
              context.report({
                node: issue.node || {loc: {line: 1, column: 0}},
                messageId: 'missingPermission',
                data: {
                  reason: issue.reason,
                  permission: issue.permission,
                },
              })
            }
          } else if (issue.type === 'domain') {
            const issueKey = `trusted_domains:${issue.domain}`
            const fixer = createManifestFixer('trusted_domains', issue.domain)

            fixer()

            if (!fixedIssues.has(issueKey)) {
              context.report({
                node: issue.node || {loc: {line: 1, column: 0}},
                messageId: 'missingTrustedDomain',
                data: {
                  reason: issue.reason,
                  domain: issue.domain,
                },
              })
            }
          }
        })
      },
    }

    function createManifestFixer(field, value) {
      return function () {
        // Explicit check: Only run if --fix flag was passed
        const hasFixFlag = process.argv.includes('--fix')
        if (!hasFixFlag) {
          return null
        }

        if (!manifestPath || manifestParseError) {
          return null
        }

        try {
          const currentManifest = JSON.parse(
            fs.readFileSync(manifestPath, 'utf8')
          )

          if (!currentManifest[field]) {
            currentManifest[field] = []
          }

          if (!currentManifest[field].includes(value)) {
            currentManifest[field].push(value)
            currentManifest[field].sort()

            const updatedContent = JSON.stringify(currentManifest, null, 2)
            fs.writeFileSync(manifestPath, `${updatedContent}\n`, 'utf8')

            // Update cache with the modified manifest and new mtime
            manifestCache = currentManifest
            manifest = currentManifest
            manifestMtimeCache = fs.statSync(manifestPath).mtimeMs

            // Mark this issue as fixed
            fixedIssues.add(`${field}:${value}`)
          }

          // Return empty array to signal we handled it
          return []
        } catch (err) {
          return null
        }
      }
    }
  },
}
