import {
  cp,
  lstat,
  mkdir,
  readFile,
  readdir,
  readlink,
  rm,
  stat,
  symlink,
  writeFile,
} from 'fs/promises'
import * as path from 'path'
import TOML from '@iarna/toml'
import { handleMcpConfigOverride } from './_mcp-config'

const cwd = process.cwd()
const projectSkillsDir = path.join(cwd, '.bricks', 'skills')
const compatibilitySkillLinks = [
  path.join(cwd, '.claude', 'skills'),
  path.join(cwd, '.codex', 'skills'),
]

async function exists(f: string) {
  try {
    await stat(f)
    return true
  } catch {
    return false
  }
}

async function pathExists(f: string) {
  try {
    await lstat(f)
    return true
  } catch {
    return false
  }
}

// Migrate old projects: remove legacy project/ directory
const oldProjectDir = path.join(cwd, 'project')
if (await exists(oldProjectDir)) {
  await rm(oldProjectDir, { recursive: true, force: true })
  console.log('Removed legacy project/ directory')
}

// handle flag --skip-copy
const skipCopyProject = process.argv.includes('--skip-copy-project')
if (skipCopyProject) {
  console.log('Skipping copy of files to ctor/')
} else {
  const libFiles = ['types', 'utils', 'index.ts']

  const ctorDir = path.join(cwd, 'ctor')
  await mkdir(ctorDir, { recursive: true })
  await Promise.all(
    libFiles.map((file) =>
      cp(path.join(import.meta.dirname, '..', file), path.join(ctorDir, file), {
        recursive: true,
      }),
    ),
  )
  console.log('Copied files to ctor/')
}

const projectMcpServer = {
  command: 'bun',
  args: [`${cwd}/node_modules/@fugood/bricks-ctor/tools/mcp-server.ts`],
}

// Codex cancels MCP tool calls it cannot prompt approval for (e.g. `codex exec`),
// so the project-local server's tools must be pre-approved in its config entry.
const codexProjectMcpServer = {
  ...projectMcpServer,
  default_tools_approval_mode: 'approve',
}

type CodexMcpConfig = {
  mcp_servers: Record<string, typeof codexProjectMcpServer | typeof projectMcpServer>
}

const hasClaudeCode = await exists(`${cwd}/CLAUDE.md`)
const hasAgentsMd = await exists(`${cwd}/AGENTS.md`)

if (hasClaudeCode || hasAgentsMd) {
  // Keep the workspace-level JSON MCP config aligned for tools that read .mcp.json.
  const mcpConfigPath = `${cwd}/.mcp.json`
  await handleMcpConfigOverride(mcpConfigPath, projectMcpServer)
}

const copyMissingSkills = async (sourceDir: string, targetDir: string) => {
  if (!(await exists(sourceDir))) return

  const packageSkills = await readdir(sourceDir, { withFileTypes: true })
  const skillsToInstall = packageSkills.filter(
    (entry) => entry.isDirectory() && !entry.name.startsWith('.'),
  )

  await mkdir(targetDir, { recursive: true })

  await Promise.all(
    skillsToInstall.map(async (entry) => {
      const targetSkillDir = path.join(targetDir, entry.name)
      if (await exists(targetSkillDir)) {
        console.log(`Skill '${entry.name}' already exists, skipping`)
      } else {
        await cp(path.join(sourceDir, entry.name), targetSkillDir, { recursive: true })
        console.log(`Installed skill '${entry.name}' to ${targetDir}/`)
      }
    }),
  )
}

const migrateSkillsDir = async (legacySkillsDir: string, canonicalSkillsDir: string) => {
  if (!(await pathExists(legacySkillsDir))) return

  const legacyStats = await lstat(legacySkillsDir)

  if (legacyStats.isSymbolicLink()) {
    const linkTarget = await readlink(legacySkillsDir)
    const resolvedTarget = path.resolve(path.dirname(legacySkillsDir), linkTarget)
    if (resolvedTarget === canonicalSkillsDir) return

    await copyMissingSkills(resolvedTarget, canonicalSkillsDir)
    await rm(legacySkillsDir, { force: true, recursive: true })
    return
  }

  if (legacyStats.isDirectory()) {
    await copyMissingSkills(legacySkillsDir, canonicalSkillsDir)
    await rm(legacySkillsDir, { force: true, recursive: true })
    return
  }

  console.warn(`Skipping skills migration for ${legacySkillsDir}; expected a directory or symlink`)
}

const ensureCompatibilitySkillLink = async (linkPath: string, targetDir: string) => {
  await mkdir(path.dirname(linkPath), { recursive: true })

  if (await pathExists(linkPath)) {
    const linkStats = await lstat(linkPath)
    if (linkStats.isSymbolicLink()) {
      const linkTarget = await readlink(linkPath)
      const resolvedTarget = path.resolve(path.dirname(linkPath), linkTarget)
      if (resolvedTarget === targetDir) return
    } else {
      console.warn(
        `Skipping skills symlink at ${linkPath}; path already exists and is not a symlink`,
      )
      return
    }
  }

  const relativeTarget = path.relative(path.dirname(linkPath), targetDir)
  const symlinkType = process.platform === 'win32' ? 'junction' : 'dir'
  await symlink(relativeTarget, linkPath, symlinkType)
  console.log(`Linked ${linkPath} -> ${relativeTarget}`)
}

const setupSkills = async () => {
  const packageSkillsDir = path.join(import.meta.dirname, '..', 'skills')
  await mkdir(projectSkillsDir, { recursive: true })
  await copyMissingSkills(packageSkillsDir, projectSkillsDir)

  for (const linkPath of compatibilitySkillLinks) {
    await migrateSkillsDir(linkPath, projectSkillsDir)
    await ensureCompatibilitySkillLink(linkPath, projectSkillsDir)
  }
}

if (hasClaudeCode || hasAgentsMd) {
  // Install project skills once and expose them through compatibility symlinks.
  await setupSkills()
}

type ClaudeSettings = {
  autoMode?: {
    environment?: string[]
    allow?: string[]
    soft_deny?: string[]
    hard_deny?: string[]
  }
  [key: string]: unknown
}

// Trusted infrastructure for auto mode's classifier. `$defaults` keeps the
// built-in environment (the working repo and its git remotes); the extra
// entries stop routine syncs to the BRICKS backend from being treated as
// external exfiltration. See https://code.claude.com/docs/en/auto-mode-config
const autoModeEnvironment = [
  '$defaults',
  'Organization: BRICKS (bricks.tools). Primary use: building BRICKS apps/modules with the bricks CLI and the local bricks-ctor MCP server.',
  'Trusted internal domains: all *.bricks.tools services — api.bricks.tools (project GraphQL API), bank.bricks.tools (config & asset Bank API), cdn.bricks.tools (asset CDN), plus the control/display/activity services. This project syncs its config and assets to these endpoints.',
]

// `.claude/settings.local.json` is per-developer local config; keep it untracked.
const ensureSettingsLocalGitignored = async () => {
  const gitignorePath = path.join(cwd, '.gitignore')
  const entry = '.claude/settings.local.json'
  const coveredBy = new Set([entry, '.claude', '.claude/', '.claude/*', '*.local.json'])

  let content = ''
  if (await exists(gitignorePath)) {
    content = await readFile(gitignorePath, 'utf-8')
    if (content.split('\n').some((line) => coveredBy.has(line.trim()))) return
  }

  const separator = content.length === 0 ? '' : content.endsWith('\n') ? '\n' : '\n\n'
  await writeFile(gitignorePath, `${content}${separator}# Claude Code local settings\n${entry}\n`)
  console.log(`Added ${entry} to .gitignore`)
}

// Pre-configure auto mode once, on initial setup. We only seed the classifier's
// trusted infrastructure — not `permissions.defaultMode: 'auto'`, which Claude
// Code ignores from project/local settings (a repo can't grant itself auto mode;
// it only takes effect from ~/.claude/settings.json). An existing autoMode block
// is left untouched so reinstalls never clobber a developer's customizations.
const setupClaudeAutoMode = async () => {
  const settingsPath = path.join(cwd, '.claude', 'settings.local.json')

  let settings: ClaudeSettings = {}
  if (await exists(settingsPath)) {
    try {
      settings = JSON.parse(await readFile(settingsPath, 'utf-8'))
    } catch {
      console.warn(`Skipping auto mode setup; ${settingsPath} is not valid JSON`)
      return
    }
    if (settings.autoMode) return
  }

  settings.autoMode = { environment: autoModeEnvironment }

  await mkdir(path.dirname(settingsPath), { recursive: true })
  await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`)
  console.log(`Set up auto mode in ${settingsPath}`)

  await ensureSettingsLocalGitignored()
}

if (hasClaudeCode) {
  // Pre-configure auto mode's trusted infrastructure for Claude Code projects.
  await setupClaudeAutoMode()
}

if (hasAgentsMd) {
  // Codex stores its project-local MCP config in .codex/config.toml.
  const defaultCodexMcpConfig = {
    mcp_servers: {
      'bricks-ctor': codexProjectMcpServer,
    },
  }

  const handleCodexMcpConfigOverride = async (mcpConfigPath: string) => {
    let mcpConfig: CodexMcpConfig
    if (await exists(mcpConfigPath)) {
      let parsed: unknown
      try {
        parsed = TOML.parse(await readFile(mcpConfigPath, 'utf-8'))
      } catch {
        // A malformed config is left untouched (with a warning) rather than overwritten with
        // the default — clobbering it would silently delete the user's other server entries.
        // Mirrors handleMcpConfigOverride's handling of a malformed .mcp.json.
        console.warn(`Skipping .codex/config.toml update; ${mcpConfigPath} is not valid TOML`)
        return
      }
      mcpConfig =
        parsed && typeof parsed === 'object' ? (parsed as CodexMcpConfig) : { mcp_servers: {} }
      if (!mcpConfig.mcp_servers || typeof mcpConfig.mcp_servers !== 'object') {
        mcpConfig.mcp_servers = {}
      }
      mcpConfig.mcp_servers['bricks-ctor'] = codexProjectMcpServer
      delete mcpConfig.mcp_servers['bricks-project']
    } else {
      mcpConfig = defaultCodexMcpConfig
    }

    await writeFile(mcpConfigPath, `${TOML.stringify(mcpConfig)}\n`)

    console.log(`Updated ${mcpConfigPath}`)
  }

  // Keep the Codex TOML MCP config aligned with the same bricks-ctor server entry.
  const codexConfigPath = `${cwd}/.codex/config.toml`
  await handleCodexMcpConfigOverride(codexConfigPath)
}

// TODO: .cursor/skills if needed
// TODO: User setting in application.json to avoid unnecessary skills/config setup
