const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')
const chalk = require('chalk')
const ora = require('ora')
const { getConfig } = require('../lib/config')
const { updateCodeLibrary, listCodeLibraries, getCodeLibrary } = require('../lib/api')
const webTranspile = require('../lib/transpile')
const resolveWebCodeType = webTranspile.resolveWebCodeType
const { lint } = require('../lib/lint')
const { resolveImageUrl } = require('../lib/media')
const {
  preprocessBilditTemplate,
  canonicalizeDoubleParenAsCastTemplates,
} = require('../lib/preprocessBilditTemplate')
const { requireDeveloperRoleForApp } = require('../lib/roles')
const { folderNameToType } = require('./library-pull')
const {
  analyzeLibraryDeps,
  printLibraryDepsHints,
  analysisHasCmsDepsWork,
  needsOptionalDepsFilePrompt,
} = require('../lib/library-deps-hint')
const { applyCmsDependenciesForCodeLibraryIfEnabled } = require('./library-apply-deps')
const { codeLibraryHasDefaultExport, NO_DEFAULT_EXPORT } = require('../lib/ensure-code-library-default-export')
const {
  assertFileIsBilditWebTemplate,
  describeBilditTemplateKindForLog,
} = require('../lib/template-meta')
const { normalizeTemplateIdArg } = require('../lib/cli-cid')

/** Match `library add` / template file naming: `Hero.template.tsx` → `Hero` */
function extractTemplateIdFromFilePath(filePath) {
  const base = path.basename(filePath, path.extname(filePath))
  return base.replace(/\.template$/, '')
}

function detectCodeType(filePath) {
  const ext = path.extname(filePath).toLowerCase()
  const extMap = {
    '.js': 'js',
    '.jsx': 'jsx',
    '.ts': 'ts',
    '.tsx': 'tsx',
    '.html': 'html',
  }
  return extMap[ext] || 'jsx'
}

async function findExistingCodeLibrary(appId, templateId) {
  try {
    const templateLibs = await listCodeLibraries(appId)
    const items = Array.isArray(templateLibs) ? templateLibs : Object.values(templateLibs || {})
    return items.find((lib) => lib.templateId === templateId || lib.snippetId === templateId)
  } catch (error) {
    return null
  }
}

/**
 * Scan a template library export tree produced by `bild library pull`: {root}/{type_folder}/*.{js,jsx,ts,tsx,html}
 * with optional sibling {basename}.json metadata.
 * @param {string} rootDir
 * @returns {Array<{ filePath: string, templateId: string, type: string, name?: string, description?: string, image?: string }>}
 */
function discoverCodeLibraryItemsFromRoot(rootDir) {
  const root = path.resolve(rootDir)
  if (!fs.existsSync(root)) {
    throw new Error(`Root folder does not exist: ${root}`)
  }
  if (!fs.statSync(root).isDirectory()) {
    throw new Error(`Root is not a directory: ${root}`)
  }

  const items = []
  const typeFolders = fs
    .readdirSync(root, { withFileTypes: true })
    .filter((d) => d.isDirectory())
    .map((d) => d.name)

  for (const typeFolder of typeFolders) {
    const typeDir = path.join(root, typeFolder)
    const entries = fs.readdirSync(typeDir, { withFileTypes: true })
    for (const ent of entries) {
      if (!ent.isFile()) continue
      const f = ent.name
      if (!/\.(js|jsx|ts|tsx|html)$/i.test(f)) continue
      const ext = path.extname(f)
      const base = path.basename(f, ext)
      const metaPath = path.join(typeDir, `${base}.json`)

      let meta = {}
      if (fs.existsSync(metaPath)) {
        try {
          meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'))
        } catch (e) {
          throw new Error(`Invalid JSON in ${metaPath}: ${e.message}`)
        }
      }

      const templateId = meta.templateId != null ? meta.templateId : base
      const type = meta.type != null ? meta.type : folderNameToType(typeFolder)

      let image
      if ('image' in meta) {
        const rawImg = meta.image
        if (rawImg === '' || rawImg == null) {
          image = undefined
        } else if (
          typeof rawImg === 'string' &&
          !/^https?:\/\//i.test(rawImg) &&
          !rawImg.startsWith('data:')
        ) {
          image = path.resolve(typeDir, rawImg)
        } else {
          image = rawImg
        }
      }

      const entry = {
        filePath: path.join(typeDir, f),
        templateId,
        type,
      }
      if ('name' in meta) entry.name = meta.name
      if ('description' in meta) entry.description = meta.description
      if (image !== undefined) entry.image = image

      items.push(entry)
    }
  }

  return items
}

async function updateFile(filePath, options) {
  const spinner = ora({ text: `Processing ${path.basename(filePath)}`, stream: process.stderr }).start()

  try {
    // Read file
    const code = fs.readFileSync(filePath, 'utf8')
    let codeType = resolveWebCodeType(code, detectCodeType(filePath))
    const appId = options.appId || getConfig().defaultAppId
    const rawCid = options.cid != null ? normalizeTemplateIdArg(options.cid) : null
    const rawTemplateId = options.templateId != null ? normalizeTemplateIdArg(options.templateId) : null
    const hasCid = rawCid != null && String(rawCid).trim() !== ''
    const hasTemplateId = rawTemplateId != null && String(rawTemplateId).trim() !== ''

    if (!appId) {
      spinner.fail('App ID is required')
      throw new Error('App ID is required. Use --app-id or set default in config.')
    }

    if (!hasCid && !hasTemplateId) {
      spinner.fail('Template ID is required')
      throw new Error('Template ID is required. Use --template-id (see `bild library update --help`).')
    }

    // Find existing template
    let existing
    if (hasCid) {
      spinner.text = 'Fetching existing template...'
      try {
        existing = await getCodeLibrary(appId, rawCid)
      } catch (e) {
        const st = e && e.response && e.response.status
        if (st === 404) {
          existing = null
        } else {
          throw e
        }
      }
      if (!existing) {
        spinner.fail(`Template with Template ID "${rawCid}" not found`)
        throw new Error(`Template with Template ID "${rawCid}" not found`)
      }
    } else {
      spinner.text = 'Finding existing template...'
      existing = await findExistingCodeLibrary(appId, rawTemplateId)
      if (!existing) {
        try {
          existing = await getCodeLibrary(appId, rawTemplateId)
        } catch (e) {
          const st = e && e.response && e.response.status
          if (st !== 404) {
            throw e
          }
        }
      }
      if (!existing) {
        spinner.fail(`Template "${rawTemplateId}" not found. Use "library add" to create a new one.`)
        throw new Error(`Template "${rawTemplateId}" not found`)
      }
    }

    const templateId = existing.templateId || existing.snippetId || rawTemplateId || rawCid

    let depsAnalysis = null
    try {
      depsAnalysis = analyzeLibraryDeps(filePath, {
        depsFile: options.depsFile,
        sitePath: options.sitePath,
        depsImportAlias: options.depsAlias,
      })
    } catch (e) {
      console.warn(chalk.dim(`Could not analyze imports for cmsDependencies hints: ${e.message}`))
    }
    if (depsAnalysis) {
      spinner.stop()
      printLibraryDepsHints(depsAnalysis, filePath)
      spinner.start(`Processing ${path.basename(filePath)}`)
    }

    let uploadCode = code
    if (options.applyDeps) {
      spinner.text = 'Applying cmsDependencies import rewrites...'
      try {
        uploadCode = await applyCmsDependenciesForCodeLibraryIfEnabled(filePath, code, options, { label: 'library update' })
      } catch (e) {
        spinner.fail(`cmsDependencies rewrite failed: ${e.message}`)
        throw e
      }
    }

    try {
      assertFileIsBilditWebTemplate(uploadCode, codeType)
    } catch (e) {
      spinner.fail(e.message)
      throw e
    }
    // ora renders `spinner.text` on the active terminal line; stop to avoid concatenating log output.
    const resumeText = spinner.text
    spinner.stop()
    console.log(chalk.dim(describeBilditTemplateKindForLog(uploadCode, codeType)))
    spinner.start(resumeText)

    uploadCode = canonicalizeDoubleParenAsCastTemplates(uploadCode, codeType)

    if (!codeLibraryHasDefaultExport(uploadCode, codeType)) {
      spinner.fail(NO_DEFAULT_EXPORT)
      const err = new Error(NO_DEFAULT_EXPORT)
      err.code = 'BILD_NO_DEFAULT_EXPORT'
      throw err
    }

    // Preprocess BILDIT template vars ($(varName:Type="default")) so code can be linted and transpiled
    const codeForValidation = preprocessBilditTemplate(uploadCode, codeType)

    if (options.lint !== false) {
      spinner.text = 'Linting code...'
      const lintResult = await lint(codeForValidation, filePath, { lintEnv: options.lintEnv })
      if (lintResult.errorCount > 0) {
        spinner.fail(`Lint failed with ${lintResult.errorCount} error(s). Fix issues before uploading.`)
        lintResult.issues
          .filter((i) => i.severity === 'error')
          .forEach((issue) => {
            console.log(chalk.red(`  Line ${issue.line}: ${issue.message}`))
          })
        throw new Error('Lint failed. Template was not updated.')
      }
      if (lintResult.warningCount > 0) {
        spinner.warn(`Found ${lintResult.warningCount} lint warning(s)`)
        lintResult.issues
          .filter((i) => i.severity === 'warning')
          .slice(0, 5)
          .forEach((issue) => {
            console.log(chalk.yellow(`  Line ${issue.line}: ${issue.message}`))
          })
      }
    }

    // Transpile code (validates that preprocessed code compiles)
    spinner.text = 'Transpiling code...'
    webTranspile(codeForValidation, codeType, templateId)

    // Get image URL (use provided, existing, or require it) - can be URL or local file path
    let imageInput = options.image || existing.image
    if (!imageInput) {
      spinner.fail('Image URL or file path is required. Use --image <url|path> option.')
      throw new Error('Image URL or file path is required. Use --image <url|path> option.')
    }

    // Resolve image URL (upload if local file)
    spinner.text = 'Processing image...'
    const imageUrl = await resolveImageUrl(imageInput, appId)

    // Update template in CMS
    spinner.text = 'Updating template...'
    const codeLibraryData = {
      name: options.name || existing.name || templateId,
      description: options.description !== undefined ? options.description : (existing.description || ''),
      snippetId: existing.snippetId || templateId, // API expects snippetId, but CLI uses template-id
      type: options.type || existing.type || 'component',
      codeType,
      code: uploadCode,
      appType: existing.appType || 'web',
      image: imageUrl,
      isCustom: existing.isCustom !== undefined ? existing.isCustom : true,
    }

    await updateCodeLibrary(appId, existing.id, codeLibraryData)

    spinner.succeed(`Successfully updated ${path.basename(filePath)}`)
    return { cid: existing.id, templateId: existing.templateId || existing.snippetId || templateId }
  } catch (error) {
    spinner.fail(`Failed to update ${path.basename(filePath)}: ${error.message}`)
    throw error
  }
}

async function updateCommand(files, options) {
  try {
    const config = getConfig()
    const appId = options.appId || config.defaultAppId

    if (!appId) {
      console.error(chalk.red('Error: App ID is required. Use --app-id or set defaultAppId in config.'))
      process.exit(1)
    }

    await requireDeveloperRoleForApp(appId)

    if (options.root) {
      let items
      try {
        items = discoverCodeLibraryItemsFromRoot(options.root)
      } catch (err) {
        console.error(chalk.red(`\n✗ ${err.message}`))
        process.exit(1)
      }
      if (items.length === 0) {
        console.error(chalk.red('No code files (.js, .jsx, .ts, .tsx, .html) found under type subfolders of --root.'))
        process.exit(1)
      }
      console.log(
        chalk.blue(
          `Batch updating ${chalk.bold(String(items.length))} template(s) from ${chalk.bold(path.resolve(options.root))}\n`,
        ),
      )
      for (const item of items) {
        await updateFile(item.filePath, {
          ...options,
          appId,
          templateId: item.templateId,
          type: item.type,
          name: item.name,
          description: item.description,
          image: item.image !== undefined ? item.image : options.image,
          cid: undefined,
        })
      }
      console.log(chalk.green('\n✓ All items updated successfully!'))
      return
    }

    let selectedTemplateId = options.templateId
    let selectedLibItem = null
    const prompt = inquirer.createPromptModule()

    // Resolve template id: explicit flag, or infer from a single file (e.g. HomeHero.template.tsx → HomeHero)
    if (!selectedTemplateId && files && files.length === 1) {
      const single = path.resolve(files[0])
      if (fs.existsSync(single) && fs.statSync(single).isFile()) {
        const fromName = extractTemplateIdFromFilePath(single)
        const templateLibs = await listCodeLibraries(appId)
        const items = Array.isArray(templateLibs) ? templateLibs : Object.values(templateLibs || {})
        selectedLibItem = items.find(
          (item) => (item.templateId || item.snippetId || item.id) === fromName,
        )
        if (selectedLibItem) {
          selectedTemplateId = fromName
        }
      }
    }

    if (!selectedTemplateId) {
      // Fetch and prompt
      console.log(chalk.blue(`Fetching templates for app ${chalk.bold(appId)}...\n`))
      const templateLibs = await listCodeLibraries(appId)

      if (!templateLibs || (Array.isArray(templateLibs) && templateLibs.length === 0)) {
        console.error(chalk.red('No templates found. Use "library add" to create new ones.'))
        process.exit(1)
      }

      const items = Array.isArray(templateLibs) ? templateLibs : Object.values(templateLibs || {})

      if (items.length === 0) {
        console.error(chalk.red('No templates found. Use "library add" to create new ones.'))
        process.exit(1)
      }

      const choices = items.map((item) => {
        const templateId = item.templateId || item.snippetId || item.id
        const displayName = `${chalk.bold(templateId)}${item.name ? ` - ${item.name}` : ''}${item.description ? ` (${item.description})` : ''}`
        return {
          name: displayName,
          value: templateId,
          short: templateId,
          item: item,
        }
      })

      const { chosenTemplateId } = await prompt([
        {
          type: 'list',
          name: 'chosenTemplateId',
          message: 'Select a template to update:',
          choices: choices.map((c) => ({ name: c.name, value: c.value, item: c.item })),
          pageSize: 20,
        },
      ])
      selectedTemplateId = chosenTemplateId
      selectedLibItem = choices.find((c) => c.value === selectedTemplateId)?.item
    } else if (!selectedLibItem) {
      const templateLibs = await listCodeLibraries(appId)
      const items = Array.isArray(templateLibs) ? templateLibs : Object.values(templateLibs || {})
      selectedLibItem = items.find(
        (item) => (item.templateId || item.snippetId || item.id) === selectedTemplateId,
      )
      if (!selectedLibItem) {
        console.error(
          chalk.red(
            `Error: Template "${selectedTemplateId}" not found in app "${appId}".`,
          ),
        )
        process.exit(1)
      }
    }

    // Now handle files
    const filePaths = []
    
    if (files && files.length > 0) {
      // Expand folders to files
      for (const file of files) {
        const resolvedPath = path.resolve(file)
        const stat = fs.statSync(resolvedPath)
        
        if (stat.isDirectory()) {
          const dirFiles = fs.readdirSync(resolvedPath)
          dirFiles.forEach((f) => {
            const fullPath = path.join(resolvedPath, f)
            if (fs.statSync(fullPath).isFile() && /\.(js|jsx|ts|tsx|html)$/.test(f)) {
              filePaths.push(fullPath)
            }
          })
        } else {
          filePaths.push(resolvedPath)
        }
      }
    }

    // If no files provided, prompt for file
    if (filePaths.length === 0) {
      const { filePath } = await prompt([
        {
          type: 'input',
          name: 'filePath',
          message: 'Enter file path to update:',
          validate: (input) => {
            if (!input) return 'File path is required'
            const resolved = path.resolve(input)
            if (!fs.existsSync(resolved)) return 'File does not exist'
            if (!fs.statSync(resolved).isFile()) return 'Path is not a file'
            return true
          },
        },
      ])
      filePaths.push(path.resolve(filePath))
    }

    console.log(chalk.blue(`\nUpdating template: ${chalk.bold(selectedTemplateId)}\n`))

    // Update each file (optional cmsDependencies prompts per file, same as library add)
    for (const filePath of filePaths) {
      let depsPreview = null
      try {
        depsPreview = analyzeLibraryDeps(filePath, {
          depsFile: options.depsFile,
          sitePath: options.sitePath || config.sitePath,
          depsImportAlias: options.depsAlias,
        })
      } catch {
        depsPreview = null
      }
      const depAnswers = await prompt([
        {
          type: 'input',
          name: 'depsFile',
          message:
            'cmsDependencies file path (used for import hints; with confirmation below, also updated when applying rewrites):',
          default: '',
          when: () => needsOptionalDepsFilePrompt(depsPreview) && !options.depsFile,
          filter: (input) => (input && String(input).trim()) || '',
        },
        {
          type: 'confirm',
          name: 'applyDepsRewrite',
          message:
            'Rewrite imports for cmsDependencies (record + namespace imports for external packages), update that file, and upload the rewritten code?',
          default: true,
          when: (answers) => {
            if (options.applyDeps) return false
            if (!analysisHasCmsDepsWork(depsPreview)) return false
            const hasDepsPath = Boolean(
              (options.depsFile && String(options.depsFile).trim()) ||
                (depsPreview && depsPreview.depsFilePath) ||
                (answers.depsFile && String(answers.depsFile).trim()),
            )
            if (!hasDepsPath) return false
            if (needsOptionalDepsFilePrompt(depsPreview) && !options.depsFile) {
              return !!(answers.depsFile && String(answers.depsFile).trim())
            }
            return true
          },
        },
      ])
      const promptedDeps = depAnswers.depsFile && depAnswers.depsFile.trim()
      const applyDeps = !!options.applyDeps || depAnswers.applyDepsRewrite === true
      await updateFile(filePath, {
        ...options,
        appId,
        templateId: selectedTemplateId,
        cid: selectedLibItem?.id,
        depsFile: options.depsFile || promptedDeps || undefined,
        sitePath: options.sitePath || config.sitePath,
        depsAlias: options.depsAlias,
        applyDeps,
      })
    }

    console.log(chalk.green('\n✓ All files updated successfully!'))
  } catch (error) {
    console.error(chalk.red(`\n✗ Update failed: ${error.message}`))
    process.exit(1)
  }
}

module.exports = updateCommand
module.exports.updateFile = updateFile
module.exports.discoverCodeLibraryItemsFromRoot = discoverCodeLibraryItemsFromRoot

