'use strict'

const os = require('os')
const path = require('path')
const fs = require('fs')
const { execSync, spawnSync } = require('child_process')
const https = require('https')
const http = require('http')

// ─── helpers ─────────────────────────────────────────────────────────────────

function run(cmd, opts = {}) {
  return spawnSync(cmd, { shell: true, stdio: 'inherit', ...opts })
}

function runCapture(cmd, opts = {}) {
  try {
    return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], ...opts }).trim()
  } catch {
    return null
  }
}

function httpGet(url, headers = {}) {
  return new Promise((resolve, reject) => {
    const lib = url.startsWith('https') ? https : http
    lib.get(url, { headers }, (res) => {
      let data = ''
      res.on('data', (chunk) => (data += chunk))
      res.on('end', () => resolve({ status: res.statusCode, body: data }))
    }).on('error', reject)
  })
}

function httpPost(url, body, headers = {}) {
  return new Promise((resolve, reject) => {
    const lib = url.startsWith('https') ? https : http
    const payload = JSON.stringify(body)
    const urlObj = new URL(url)
    const opts = {
      hostname: urlObj.hostname,
      path: urlObj.pathname + urlObj.search,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(payload),
        ...headers,
      },
    }
    const req = lib.request(opts, (res) => {
      let data = ''
      res.on('data', (chunk) => (data += chunk))
      res.on('end', () => resolve({ status: res.statusCode, body: data }))
    })
    req.on('error', reject)
    req.write(payload)
    req.end()
  })
}

// ─── Node version check ───────────────────────────────────────────────────────

function checkNodeVersion() {
  const [major] = process.versions.node.split('.').map(Number)
  if (major < 20) {
    console.error(`❌  Node.js >= 20 required. Current: ${process.version}`)
    process.exit(1)
  }
  console.log(`✅  Node.js ${process.version}`)
}

// ─── npm install + build ──────────────────────────────────────────────────────

function npmInstallAndBuild() {
  const botDir = __dirname
  console.log('\n📦  Running npm install...')
  const r1 = run('npm install', { cwd: botDir })
  if (r1.status !== 0) { console.error('❌  npm install failed'); process.exit(1) }

  console.log('\n🔨  Running npm run build (tsc)...')
  const r2 = run('npm run build', { cwd: botDir })
  if (r2.status !== 0) { console.error('❌  npm run build failed'); process.exit(1) }
  console.log('✅  Build complete')
}

// ─── inquirer shim (dynamic require after npm install) ───────────────────────

async function getInquirer() {
  try {
    // inquirer v8 is CJS-compatible; v9+ is ESM-only — try both
    return require('inquirer')
  } catch {
    console.error('❌  inquirer not found. Make sure it is listed in bot/package.json dependencies.')
    process.exit(1)
  }
}

// ─── interactive config ───────────────────────────────────────────────────────

const MODEL_DEFAULTS = {
  anthropic: 'claude-sonnet-4-5',
  openai: 'gpt-4o',
  ollama: 'llama3',
}

async function promptConfig() {
  const inquirer = await getInquirer()

  const answers = await inquirer.prompt([
    {
      type: 'input',
      name: 'slackSessionName',
      message: 'Slack session name (e.g. cijira):',
      validate: (v) => v.trim().length > 0 || 'Required',
    },
    {
      type: 'password',
      name: 'slackBotToken',
      message: 'Slack Bot Token (xoxb-...):',
      validate: (v) => v.trim().startsWith('xoxb-') || 'Must start with xoxb-',
    },
    {
      type: 'password',
      name: 'slackAppToken',
      message: 'Slack App Token (xapp-...):',
      validate: (v) => v.trim().startsWith('xapp-') || 'Must start with xapp-',
    },
    {
      type: 'input',
      name: 'jiraBaseUrl',
      message: 'Jira base URL (e.g. https://myorg.atlassian.net):',
      validate: (v) => v.trim().startsWith('http') || 'Must be a URL',
    },
    {
      type: 'input',
      name: 'jiraProjectKey',
      message: 'Jira project key:',
      validate: (v) => v.trim().length > 0 || 'Required',
    },
    {
      type: 'input',
      name: 'jiraBacklogColumn',
      message: 'Jira backlog column name:',
      default: 'Ideia/Backlog',
    },
    {
      type: 'password',
      name: 'jiraApiToken',
      message: 'Jira API token:',
      validate: (v) => v.trim().length > 0 || 'Required',
    },
    {
      type: 'input',
      name: 'jiraUserEmail',
      message: 'Jira user email:',
      validate: (v) => v.includes('@') || 'Must be an email',
    },
    {
      type: 'input',
      name: 'bitbucketWorkspace',
      message: 'Bitbucket workspace:',
      validate: (v) => v.trim().length > 0 || 'Required',
    },
    {
      type: 'input',
      name: 'bitbucketRepoSlug',
      message: 'Bitbucket repo slug:',
      validate: (v) => v.trim().length > 0 || 'Required',
    },
    {
      type: 'input',
      name: 'bitbucketBaseBranch',
      message: 'Bitbucket base branch:',
      default: 'homolog',
    },
    {
      type: 'password',
      name: 'bitbucketAppPassword',
      message: 'Bitbucket API token with scopes (bitbucket.org/account/settings/api-tokens — Repositories:RW, Pull requests:RW, Account:R):',
      validate: (v) => v.trim().length > 0 || 'Required',
    },
    {
      type: 'input',
      name: 'bitbucketUsername',
      message: 'Bitbucket email (email da conta, nao o username):',
      validate: (v) => v.trim().length > 0 || 'Required',
    },
    {
      type: 'list',
      name: 'llmProvider',
      message: 'LLM provider:',
      choices: ['anthropic', 'openai', 'ollama'],
    },
    {
      type: 'input',
      name: 'llmModel',
      message: 'Model name:',
      default: (ans) => MODEL_DEFAULTS[ans.llmProvider] || '',
    },
    {
      type: 'password',
      name: 'llmApiKey',
      message: 'API key (leave blank for ollama):',
      when: (ans) => ans.llmProvider !== 'ollama',
      default: '',
    },
  ])

  if (!answers.llmApiKey) answers.llmApiKey = ''
  return answers
}

// ─── connection validation ────────────────────────────────────────────────────

async function validateConnections(cfg) {
  const results = {}

  // Slack
  try {
    const r = await httpGet('https://slack.com/api/auth.test', {
      Authorization: `Bearer ${cfg.slackBotToken}`,
    })
    const body = JSON.parse(r.body)
    results.slack = body.ok === true
  } catch {
    results.slack = false
  }
  console.log(results.slack ? '✅  Slack' : '❌  Slack')

  // Jira
  try {
    const creds = Buffer.from(`${cfg.jiraUserEmail}:${cfg.jiraApiToken}`).toString('base64')
    const r = await httpGet(`${cfg.jiraBaseUrl.replace(/\/$/, '')}/rest/api/3/myself`, {
      Authorization: `Basic ${creds}`,
      Accept: 'application/json',
    })
    results.jira = r.status >= 200 && r.status < 300
  } catch {
    results.jira = false
  }
  console.log(results.jira ? '✅  Jira' : '❌  Jira')

  // Bitbucket
  try {
    const creds = Buffer.from(`${cfg.bitbucketUsername}:${cfg.bitbucketAppPassword}`).toString('base64')
    const r = await httpGet(`https://api.bitbucket.org/2.0/repositories/${cfg.bitbucketWorkspace}/${cfg.bitbucketRepoSlug}`, {
      Authorization: `Basic ${creds}`,
    })
    results.bitbucket = r.status >= 200 && r.status < 300
  } catch {
    results.bitbucket = false
  }
  console.log(results.bitbucket ? '✅  Bitbucket' : '❌  Bitbucket')

  // LLM
  try {
    if (cfg.llmProvider === 'anthropic') {
      const r = await httpPost(
        'https://api.anthropic.com/v1/messages',
        {
          model: cfg.llmModel,
          max_tokens: 8,
          messages: [{ role: 'user', content: 'ping' }],
        },
        {
          'x-api-key': cfg.llmApiKey,
          'anthropic-version': '2023-06-01',
        }
      )
      results.llm = r.status >= 200 && r.status < 300
    } else if (cfg.llmProvider === 'openai') {
      const r = await httpGet('https://api.openai.com/v1/models', {
        Authorization: `Bearer ${cfg.llmApiKey}`,
      })
      results.llm = r.status >= 200 && r.status < 300
    } else {
      // ollama — list models
      const r = await httpGet('http://localhost:11434/api/tags')
      results.llm = r.status >= 200 && r.status < 300
    }
  } catch {
    results.llm = false
  }
  console.log(results.llm ? `✅  LLM (${cfg.llmProvider})` : `❌  LLM (${cfg.llmProvider})`)

  return results
}

// ─── retry loop for failed services ──────────────────────────────────────────

async function retryFailedServices(cfg, results) {
  const inquirer = await getInquirer()
  const failed = Object.entries(results).filter(([, ok]) => !ok).map(([k]) => k)
  if (failed.length === 0) return cfg

  const { retry } = await inquirer.prompt([
    {
      type: 'confirm',
      name: 'retry',
      message: `Some connections failed (${failed.join(', ')}). Re-enter credentials?`,
      default: true,
    },
  ])
  if (!retry) return cfg

  const updatedCfg = { ...cfg }

  for (const service of failed) {
    if (service === 'slack') {
      const ans = await inquirer.prompt([
        { type: 'password', name: 'slackBotToken', message: 'Slack Bot Token:', validate: (v) => v.trim().length > 0 },
        { type: 'password', name: 'slackAppToken', message: 'Slack App Token:', validate: (v) => v.trim().length > 0 },
      ])
      Object.assign(updatedCfg, ans)
    } else if (service === 'jira') {
      const ans = await inquirer.prompt([
        { type: 'input', name: 'jiraBaseUrl', message: 'Jira base URL:', default: cfg.jiraBaseUrl },
        { type: 'password', name: 'jiraApiToken', message: 'Jira API token:' },
        { type: 'input', name: 'jiraUserEmail', message: 'Jira user email:', default: cfg.jiraUserEmail },
      ])
      Object.assign(updatedCfg, ans)
    } else if (service === 'bitbucket') {
      const ans = await inquirer.prompt([
        { type: 'password', name: 'bitbucketAppPassword', message: 'Bitbucket API token with scopes:' },
        { type: 'input', name: 'bitbucketUsername', message: 'Bitbucket email (email da conta):', default: cfg.bitbucketUsername },
      ])
      Object.assign(updatedCfg, ans)
    } else if (service === 'llm') {
      const ans = await inquirer.prompt([
        { type: 'password', name: 'llmApiKey', message: 'LLM API key:' },
      ])
      Object.assign(updatedCfg, ans)
    }
  }

  console.log('\nRe-validating...')
  const newResults = await validateConnections(updatedCfg)
  return retryFailedServices(updatedCfg, newResults)
}

// ─── write config files ───────────────────────────────────────────────────────

function writeConfigFiles(cfg, projectRoot) {
  const botDir = path.join(projectRoot, '.sdd', 'bot')
  fs.mkdirSync(botDir, { recursive: true })
  fs.mkdirSync(path.join(botDir, 'versions'), { recursive: true })

  const yaml = `# sdd-bot configuration — generated by install.js
# DO NOT commit this file.

slack:
  session_name: "${cfg.slackSessionName}"
  bot_token: "${cfg.slackBotToken}"
  app_token: "${cfg.slackAppToken}"

jira:
  base_url: "${cfg.jiraBaseUrl}"
  project_key: "${cfg.jiraProjectKey}"
  backlog_column: "${cfg.jiraBacklogColumn}"
  api_token: "${cfg.jiraApiToken}"
  user: "${cfg.jiraUserEmail}"

bitbucket:
  workspace: "${cfg.bitbucketWorkspace}"
  repo_slug: "${cfg.bitbucketRepoSlug}"
  base_branch: "${cfg.bitbucketBaseBranch}"
  app_password: "${cfg.bitbucketAppPassword}"
  user: "${cfg.bitbucketUsername}"

llm:
  provider: "${cfg.llmProvider}"
  model: "${cfg.llmModel}"
  api_key: "${cfg.llmApiKey}"

sdd:
  worktrees_root: "${projectRoot.replace(/\\/g, '\\\\')}\\\\.worktree"
`

  fs.writeFileSync(path.join(botDir, 'sdd-bot.config.yaml'), yaml, 'utf8')
  console.log('✅  Written .sdd/bot/sdd-bot.config.yaml')

  const sessionsPath = path.join(botDir, 'sessions.json')
  if (!fs.existsSync(sessionsPath)) {
    fs.writeFileSync(sessionsPath, '{}', 'utf8')
    console.log('✅  Created .sdd/bot/sessions.json')
  }
}

function writeEcosystemConfig(projectRoot) {
  const ecosystemPath = path.join(__dirname, 'ecosystem.config.js')
  const content = `module.exports = {
  apps: [{
    name: 'sdd-bot',
    script: './dist/index.js',
    cwd: __dirname,
    env: { NODE_ENV: 'production', SDD_BOT_ROOT: ${JSON.stringify(projectRoot)} },
    restart_delay: 5000,
    max_restarts: 10,
  }]
}
`
  fs.writeFileSync(ecosystemPath, content, 'utf8')
  console.log('✅  Written bot/ecosystem.config.js')
}

// Configura o remote 'origin' do projeto para autenticar com o API token do
// Bitbucket via o username estático x-bitbucket-api-token-auth — o método correto
// para Git over HTTPS (o e-mail da conta só funciona na REST API). Sem isso, o
// fetch/push do bot falha e a criação de worktree quebra. App passwords estão
// sendo descontinuados pelo Bitbucket, por isso usamos API token com scopes.
function configureGitRemote(cfg, projectRoot) {
  const token = cfg.bitbucketAppPassword
  const ws = cfg.bitbucketWorkspace
  const repo = cfg.bitbucketRepoSlug
  if (!token || !ws || !repo) {
    console.warn('⚠️   Dados do Bitbucket incompletos — pulei a configuração do remote git')
    return
  }
  const url = `https://x-bitbucket-api-token-auth:${token}@bitbucket.org/${ws}/${repo}.git`
  const hasOrigin = runCapture('git remote get-url origin', { cwd: projectRoot })
  const action = hasOrigin ? 'set-url' : 'add'
  // args como array (sem shell) — evita problemas de escaping do token (que tem
  // caracteres como '=' e '_') e não expõe o token na linha de comando do shell.
  const r = spawnSync('git', ['remote', action, 'origin', url], {
    cwd: projectRoot,
    stdio: 'pipe',
  })
  if (r.status === 0) {
    console.log('✅  remote git origin configurado com API token (x-bitbucket-api-token-auth)')
    const test = spawnSync('git', ['ls-remote', '--heads', 'origin'], {
      cwd: projectRoot,
      stdio: 'pipe',
      env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
    })
    console.log(
      test.status === 0
        ? '✅  autenticação git validada (fetch/push prontos para o bot)'
        : '⚠️   git ls-remote falhou — confirme o token e os scopes (Repositories: Read+Write)'
    )
  } else {
    console.warn('⚠️   não consegui configurar o remote git origin automaticamente')
  }
}

function updateGitignore(projectRoot) {
  const gitignorePath = path.join(projectRoot, '.gitignore')
  const entries = [
    '.sdd/bot/sdd-bot.config.yaml',
    '.sdd/bot/sessions.json',
    '.sdd/bot/versions/',
    'bot/node_modules/',
    'bot/dist/',
    'bot/ecosystem.config.js',
  ]

  let existing = ''
  if (fs.existsSync(gitignorePath)) {
    existing = fs.readFileSync(gitignorePath, 'utf8')
  }

  const toAdd = entries.filter((e) => !existing.includes(e))
  if (toAdd.length > 0) {
    const addition = '\n# sdd-bot (generated by install.js)\n' + toAdd.join('\n') + '\n'
    fs.appendFileSync(gitignorePath, addition, 'utf8')
    console.log('✅  .gitignore updated')
  } else {
    console.log('✅  .gitignore already up to date')
  }
}

// ─── pm2 setup ────────────────────────────────────────────────────────────────

function ensurePm2() {
  const found = runCapture('pm2 --version')
  if (found) {
    console.log(`✅  pm2 already installed (${found})`)
    return
  }
  console.log('📦  Installing pm2 globally...')
  const r = run('npm install -g pm2')
  if (r.status !== 0) { console.error('❌  pm2 install failed'); process.exit(1) }
  console.log('✅  pm2 installed')
}

function setupPm2Autostart() {
  const platform = os.platform()
  console.log(`\n⚙️   Configuring pm2 autostart for platform: ${platform}`)

  // start the app first so pm2 save has something to persist
  const startResult = run('pm2 start ecosystem.config.js', { cwd: __dirname })
  if (startResult.status !== 0) {
    console.warn('⚠️   pm2 start returned non-zero — continuing anyway')
  }

  if (platform === 'win32') {
    // pm2-windows-startup or native Task Scheduler fallback
    const hasWindowsStartup = runCapture('npm list -g pm2-windows-startup')
    if (!hasWindowsStartup || hasWindowsStartup.includes('empty')) {
      console.log('📦  Installing pm2-windows-startup...')
      run('npm install -g pm2-windows-startup')
      run('pm2-startup install')
    } else {
      run('pm2-startup install')
    }
  } else {
    // darwin and linux: pm2 startup generates the appropriate init system command
    const startupOut = runCapture('pm2 startup')
    if (startupOut) {
      // pm2 prints a sudo command — try to run it automatically
      const match = startupOut.match(/sudo\s+.+/)
      if (match) {
        console.log('Running generated startup command (may require sudo):')
        console.log(match[0])
        run(match[0])
      }
    }
  }

  run('pm2 save')
  console.log('✅  pm2 autostart configured and process list saved')
}

// ─── main install flow ────────────────────────────────────────────────────────

async function install() {
  console.log('=== sdd-bot installer ===\n')

  checkNodeVersion()

  npmInstallAndBuild()

  const cfg = await promptConfig()

  console.log('\nValidating connections...')
  const results = await validateConnections(cfg)
  const finalCfg = await retryFailedServices(cfg, results)

  // detect project root (two levels up from bot/)
  const projectRoot = path.resolve(__dirname, '..')

  writeConfigFiles(finalCfg, projectRoot)
  writeEcosystemConfig(projectRoot)
  updateGitignore(projectRoot)
  configureGitRemote(finalCfg, projectRoot)

  ensurePm2()
  setupPm2Autostart()

  console.log('\n🎉  sdd-bot installation complete!')
}

// ─── exports ──────────────────────────────────────────────────────────────────

module.exports = { install, validateConnections, setupPm2Autostart }

if (require.main === module) {
  install().catch((err) => {
    console.error('Installation failed:', err)
    process.exit(1)
  })
}
