// bot/src/config.ts — carrega e valida sdd-bot.config.yaml com singleton e busca ascendente de raiz

import fs from 'fs'
import path from 'path'
import yaml from 'js-yaml'
import { BotConfig } from './types'

const CONFIG_RELATIVE_PATH = path.join('.sdd', 'bot', 'sdd-bot.config.yaml')
const ROOT_MARKERS = ['.sdd', 'sdd.config.yaml']

/**
 * Sobe o árcore de diretórios a partir de `startDir` até encontrar um dos marcadores
 * que indicam a raiz do projeto SDD. Retorna null se chegar ao filesystem root sem encontrar.
 */
function findProjectRoot(startDir: string): string | null {
  let current = path.resolve(startDir)

  while (true) {
    for (const marker of ROOT_MARKERS) {
      if (fs.existsSync(path.join(current, marker))) {
        return current
      }
    }

    const parent = path.dirname(current)
    if (parent === current) {
      // chegou ao filesystem root
      return null
    }
    current = parent
  }
}

/**
 * Valida que todas as chaves obrigatórias de alto nível estão presentes no objeto carregado.
 * Lança erro descritivo indicando qual seção está ausente.
 */
function validateConfig(raw: unknown, configPath: string): BotConfig {
  if (typeof raw !== 'object' || raw === null) {
    throw new Error(
      `[sdd-bot] Configuração inválida em "${configPath}": o arquivo não é um objeto YAML válido.`
    )
  }

  const obj = raw as Record<string, unknown>
  const requiredSections: Array<keyof BotConfig> = ['slack', 'jira', 'bitbucket', 'llm', 'sdd']

  for (const section of requiredSections) {
    if (!(section in obj) || obj[section] == null) {
      throw new Error(
        `[sdd-bot] Seção obrigatória ausente no config "${configPath}": "${section}". ` +
        `Verifique o template em .sdd/bot/sdd-bot.config.example.yaml.`
      )
    }
  }

  const slack = obj['slack'] as Record<string, unknown>
  const jira = obj['jira'] as Record<string, unknown>
  const bitbucket = obj['bitbucket'] as Record<string, unknown>
  const llm = obj['llm'] as Record<string, unknown>
  const sdd = obj['sdd'] as Record<string, unknown>

  // Validações de campos críticos por seção
  const slackRequired = ['session_name', 'bot_token', 'app_token']
  for (const field of slackRequired) {
    if (!slack[field]) {
      throw new Error(`[sdd-bot] Campo obrigatório ausente em slack.${field} (arquivo: "${configPath}").`)
    }
  }

  const jiraRequired = ['base_url', 'project_key', 'backlog_column', 'api_token', 'user']
  for (const field of jiraRequired) {
    if (!jira[field]) {
      throw new Error(`[sdd-bot] Campo obrigatório ausente em jira.${field} (arquivo: "${configPath}").`)
    }
  }

  const bitbucketRequired = ['workspace', 'repo_slug', 'base_branch', 'app_password', 'user']
  for (const field of bitbucketRequired) {
    if (!bitbucket[field]) {
      throw new Error(`[sdd-bot] Campo obrigatório ausente em bitbucket.${field} (arquivo: "${configPath}").`)
    }
  }

  const validProviders = ['anthropic', 'openai', 'ollama']
  if (!llm['provider'] || !validProviders.includes(llm['provider'] as string)) {
    throw new Error(
      `[sdd-bot] llm.provider inválido em "${configPath}". ` +
      `Valores aceitos: ${validProviders.join(', ')}.`
    )
  }

  if (!llm['model']) {
    throw new Error(`[sdd-bot] Campo obrigatório ausente em llm.model (arquivo: "${configPath}").`)
  }

  if (!sdd['worktrees_root']) {
    throw new Error(`[sdd-bot] Campo obrigatório ausente em sdd.worktrees_root (arquivo: "${configPath}").`)
  }

  return obj as unknown as BotConfig
}

/**
 * Singleton que carrega, valida e armazena em cache a configuração do bot.
 *
 * Estratégia de localização do projeto (em ordem de prioridade):
 *   1. Variável de ambiente SDD_BOT_ROOT
 *   2. Busca ascendente a partir de process.cwd() por marcadores (.sdd/ ou sdd.config.yaml)
 *   3. Erro claro se nenhum marcador for encontrado
 */
export class Config {
  private static instance: Config | null = null
  private config: BotConfig
  private projectRoot: string
  private configPath: string

  private constructor() {
    this.projectRoot = this.resolveProjectRoot()
    this.configPath = path.join(this.projectRoot, CONFIG_RELATIVE_PATH)
    this.config = this.load()
  }

  static getInstance(): Config {
    if (!Config.instance) {
      Config.instance = new Config()
    }
    return Config.instance
  }

  /**
   * Expõe a configuração validada.
   */
  get(): BotConfig {
    return this.config
  }

  /**
   * Retorna o caminho absoluto da raiz do projeto detectada.
   */
  getProjectRoot(): string {
    return this.projectRoot
  }

  /**
   * Retorna o caminho absoluto do arquivo de configuração carregado.
   */
  getConfigPath(): string {
    return this.configPath
  }

  /**
   * Força recarga do config do disco (útil em hot-reload ou testes).
   */
  reload(): BotConfig {
    this.config = this.load()
    return this.config
  }

  /**
   * Descarta a instância cached (útil em testes unitários).
   */
  static reset(): void {
    Config.instance = null
  }

  private resolveProjectRoot(): string {
    // 1. Variável de ambiente tem prioridade máxima
    const envRoot = process.env['SDD_BOT_ROOT']
    if (envRoot) {
      const resolved = path.resolve(envRoot)
      if (!fs.existsSync(resolved)) {
        throw new Error(
          `[sdd-bot] SDD_BOT_ROOT aponta para um diretório inexistente: "${resolved}".`
        )
      }
      return resolved
    }

    // 2. Busca ascendente a partir do cwd
    const found = findProjectRoot(process.cwd())
    if (!found) {
      throw new Error(
        `[sdd-bot] Não foi possível localizar a raiz do projeto SDD. ` +
        `Certifique-se de executar o bot dentro de um projeto que contenha ".sdd/" ou "sdd.config.yaml", ` +
        `ou defina a variável de ambiente SDD_BOT_ROOT com o caminho absoluto do projeto.`
      )
    }

    return found
  }

  private load(): BotConfig {
    if (!fs.existsSync(this.configPath)) {
      throw new Error(
        `[sdd-bot] Arquivo de configuração não encontrado: "${this.configPath}". ` +
        `Crie o arquivo a partir do template: .sdd/bot/sdd-bot.config.example.yaml`
      )
    }

    let raw: unknown
    try {
      const content = fs.readFileSync(this.configPath, 'utf-8')
      raw = yaml.load(content)
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err)
      throw new Error(
        `[sdd-bot] Falha ao ler ou parsear o arquivo de configuração "${this.configPath}": ${message}`
      )
    }

    return validateConfig(raw, this.configPath)
  }
}

/**
 * Carrega e retorna a configuração do bot (usa singleton interno).
 * Equivalente a `Config.getInstance().get()`.
 */
export function loadConfig(): BotConfig {
  return Config.getInstance().get()
}

/**
 * Retorna o prefixo de sessão (primeiros 4 caracteres de slack.session_name).
 * Usado para nomear canais, branches e worktrees de forma consistente.
 *
 * Exemplo: session_name "cijira" → prefixo "ciji"
 */
export function getSessionPrefix(config: BotConfig): string {
  const name = config.slack.session_name
  if (!name || name.length < 4) {
    throw new Error(
      `[sdd-bot] slack.session_name deve ter ao menos 4 caracteres para gerar o prefixo. ` +
      `Valor atual: "${name}".`
    )
  }
  return name.slice(0, 4)
}
