import { spawn } from 'node:child_process'
import { mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { z } from 'zod/v4'
import { getGitCommandEnv, getGitWorkDir } from './hooks.js'
export { configure, resetConfigure } from './hooks.js'
export type { GitToolConfigure } from './hooks.js'
export { isEnabledInAgentConfig } from './agentConfig.js'
export { initWsSession } from './wsInit.js'
export type { GitToolWsInitContext } from './wsInit.js'
export {
  ensureConfigured,
  prepareForAgent,
  resolveGitWorkDir,
  readGitCredentials,
  buildGitCredentialEnv,
  withHttpsCredentials,
  redactUrlCredentials,
} from './workspace.js'
export type { GitCredentials, PrepareForAgentResult } from './workspace.js'

/*  ------------------------------------------------------------------ */
/*  JSDoc type references to main-project types (compile-time only)     */
/*  ------------------------------------------------------------------ */

/**
 * @typedef {import('../../../src/Tool.js').Tool} Tool
 * @typedef {import('../../../src/Tool.js').ToolUseContext} ToolUseContext
 * @typedef {import('../../../src/Tool.js').ToolResult} ToolResult
 * @typedef {import('../../../src/Tool.js').ValidationResult} InternalValidationResult
 * @typedef {import('../../../src/Tool.js').PermissionResult} PermissionResult
 */

/*  ------------------------------------------------------------------ */
/*  Types & helpers (no change in behavior)                             */
/*  ------------------------------------------------------------------ */

type ToolInput = {
  commands: string[]
}

type CommandSnapshot = {
  command: string
  stdout: string
  stderr: string
  exitCode: number
}

type ToolOutput = {
  cwd: string
  resultFile: string
  steps: CommandSnapshot[]
}

/** 读取 git 工作区；未 configure 时抛错（fail-fast） */
function requireGitWorkDir(): string {
  const dir = getGitWorkDir()
  if (!dir || dir.trim() === '') {
    throw new Error('GitTool: git work directory not configured')
  }
  return dir
}

function validateCommand(command: string): string {
  const trimmed = command.trim()
  if (!trimmed) throw new Error('command must not be empty')
  if (trimmed.includes('\n')) {
    throw new Error(`command must be single-line: ${command}`)
  }
  return trimmed
}

async function runCommand(params: {
  cwd: string
  command: string
  signal: AbortSignal
  env?: Record<string, string>
}): Promise<CommandSnapshot> {
  const { cwd, command, signal, env = {} } = params
  return await new Promise<CommandSnapshot>((resolve, reject) => {
    const proc = spawn('bash', ['-lc', command], {
      cwd,
      stdio: ['ignore', 'pipe', 'pipe'],
      signal,
      env: { ...process.env, ...env },
    })
    let stdout = ''
    let stderr = ''
    proc.stdout.on('data', c => (stdout += String(c)))
    proc.stderr.on('data', c => (stderr += String(c)))
    proc.on('error', reject)
    proc.on('close', code => {
      const exitCode = typeof code === 'number' ? code : 1
      if (exitCode !== 0) {
        reject(
          new Error(
            `command failed: ${command}\nexit=${exitCode}\nstderr=${stderr || '(empty)'}`,
          ),
        )
        return
      }
      resolve({ command, stdout, stderr, exitCode })
    })
  })
}

async function runCommandAllowFailure(params: {
  cwd: string
  command: string
  signal: AbortSignal
  env?: Record<string, string>
}): Promise<CommandSnapshot> {
  const { cwd, command, signal, env = {} } = params
  return await new Promise<CommandSnapshot>((resolve, reject) => {
    const proc = spawn('bash', ['-lc', command], {
      cwd,
      stdio: ['ignore', 'pipe', 'pipe'],
      signal,
      env: { ...process.env, ...env },
    })
    let stdout = ''
    let stderr = ''
    proc.stdout.on('data', c => (stdout += String(c)))
    proc.stderr.on('data', c => (stderr += String(c)))
    proc.on('error', reject)
    proc.on('close', code => {
      resolve({
        command,
        stdout,
        stderr,
        exitCode: typeof code === 'number' ? code : 1,
      })
    })
  })
}

async function writeResultFile(params: {
  toolResultsDir: string
  payload: unknown
}): Promise<string> {
  const { toolResultsDir, payload } = params
  await mkdir(toolResultsDir, { recursive: true })
  const filePath = join(toolResultsDir, `git-tool-git-${Date.now()}.json`)
  await writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8')
  return filePath
}

const READ_ONLY_SUBCOMMANDS = new Set([
  'status', 'diff', 'log', 'show', 'branch', 'rev-parse', 'ls-files',
  'ls-tree', 'cat-file', 'blame', 'reflog', 'shortlog', 'describe',
  'grep', 'remote', 'tag',
])

/** 常见 git 子命令：裸子命令输入时自动补 git 前缀 */
const GIT_SUBCOMMANDS = new Set([
  ...READ_ONLY_SUBCOMMANDS,
  'add', 'commit', 'push', 'pull', 'fetch', 'checkout', 'merge',
  'reset', 'revert', 'stash', 'cherry-pick', 'rebase', 'clone',
  'init', 'mv', 'rm', 'clean', 'switch', 'restore', 'submodule',
])

/**
 * 规范化 git 命令：完整 git 命令原样返回；裸子命令（如 log）补 git 前缀。
 */
function normalizeGitCommand(command: string): string {
  const trimmed = validateCommand(command)
  if (/^git(\s|$)/.test(trimmed)) {
    return trimmed
  }
  const firstToken = trimmed.split(/\s+/)[0] ?? ''
  if (GIT_SUBCOMMANDS.has(firstToken)) {
    return `git ${trimmed}`
  }
  throw new Error(
    `command must start with "git " or a known git subcommand: ${command}`,
  )
}

function isReadOnlyCommands(commands: string[]): boolean {
  if (!commands || commands.length === 0) return false
  return commands.every(command => {
    const normalized = normalizeGitCommand(command)
    const tokens = normalized.split(/\s+/)
    return READ_ONLY_SUBCOMMANDS.has(tokens[1] ?? '')
  })
}

/**
 * 从 gitWorkDir 读取当前分支名（非 session cwd 上层仓库）。
 * 非 git 仓库或 detached 时返回 null。
 */
async function readGitBranchFromWorkDir(
  workDir: string,
  signal: AbortSignal,
  gitEnv: Record<string, string>,
): Promise<string | null> {
  const inRepo = await runCommandAllowFailure({
    cwd: workDir,
    command: 'git rev-parse --is-inside-work-tree',
    signal,
    env: gitEnv,
  })
  if (inRepo.exitCode !== 0 || inRepo.stdout.trim() !== 'true') {
    return null
  }
  const branch = await runCommandAllowFailure({
    cwd: workDir,
    command: 'git branch --show-current',
    signal,
    env: gitEnv,
  })
  if (branch.exitCode !== 0) {
    return null
  }
  const name = branch.stdout.trim()
  return name || null
}

/** 拼接到 GitTool prompt 顶部的 gitWorkDir + gitBranch 行（仅基于 gitWorkDir） */
async function buildGitWorkDirBranchLines(): Promise<string> {
  const workDir = getGitWorkDir()
  if (!workDir) {
    return 'gitWorkDir: NOT_CONFIGURED\ngitBranch: NOT_CONFIGURED'
  }
  const abortController = new AbortController()
  const gitEnv = getGitCommandEnv()
  const branch = await readGitBranchFromWorkDir(
    workDir,
    abortController.signal,
    gitEnv,
  )
  const branchLine =
    branch === null
      ? 'gitBranch: NOT_GIT_REPO'
      : `gitBranch: ${branch}`
  return `gitWorkDir: ${workDir}\n${branchLine}`
}

async function buildGitStatusForPrompt(): Promise<string> {
  const workDir = getGitWorkDir()
  if (!workDir) {
    return 'repoStatus: NOT_CONFIGURED'
  }
  const abortController = new AbortController()
  const gitEnv = getGitCommandEnv()
  const inRepo = await runCommandAllowFailure({
    cwd: workDir,
    command: 'git rev-parse --is-inside-work-tree',
    signal: abortController.signal,
    env: gitEnv,
  })
  if (inRepo.exitCode !== 0 || inRepo.stdout.trim() !== 'true') {
    return 'repoStatus: NOT_GIT_REPO'
  }
  const branchName = await readGitBranchFromWorkDir(
    workDir,
    abortController.signal,
    gitEnv,
  )
  const [defaultBranchRef, status, log] = await Promise.all([
    runCommandAllowFailure({
      cwd: workDir, command: 'git rev-parse --abbrev-ref origin/HEAD', signal: abortController.signal, env: gitEnv,
    }),
    runCommandAllowFailure({
      cwd: workDir, command: 'git status --short', signal: abortController.signal, env: gitEnv,
    }),
    runCommandAllowFailure({
      cwd: workDir, command: 'git log --oneline -5', signal: abortController.signal, env: gitEnv,
    }),
  ])
  const defaultBranch =
    defaultBranchRef.exitCode === 0
      ? defaultBranchRef.stdout.trim().replace('origin/', '')
      : `UNAVAILABLE(${defaultBranchRef.stderr.trim() || 'origin/HEAD unresolved'})`
  const statusText = status.exitCode === 0 ? status.stdout.trim() || '(clean)' : `(failed) ${status.stderr.trim()}`
  const recentCommits = log.exitCode === 0 ? log.stdout.trim() || '(empty)' : `(failed) ${log.stderr.trim()}`
  const gitBranchName =
    branchName === null
      ? 'NOT_GIT_REPO'
      : branchName || '(detached-or-empty)'
  return [
    `gitBranch: ${gitBranchName}`,
    `defaultBranch: ${defaultBranch}`,
    `statusShort:\n${statusText}`,
    `recentCommits:\n${recentCommits}`,
  ].join('\n\n')
}

/*  ------------------------------------------------------------------ */
/*  JSON Schema → Zod converter (moved from sdkAdapter.ts)              */
/*  ------------------------------------------------------------------ */

function jsonSchemaToZod(schema: any): z.ZodType<any> {
  if (schema && typeof schema === 'object' && '_def' in schema) {
    return schema as z.ZodType<any>
  }
  if (schema.type === 'string') {
    let zs = z.string()
    if (schema.minLength !== undefined) zs = zs.min(schema.minLength)
    if (schema.maxLength !== undefined) zs = zs.max(schema.maxLength)
    return zs
  }
  if (schema.type === 'number' || schema.type === 'integer') return z.number()
  if (schema.type === 'boolean') return z.boolean()
  if (schema.type === 'array') {
    let za = z.array(schema.items ? jsonSchemaToZod(schema.items) : z.any())
    if (schema.minItems !== undefined) za = za.min(schema.minItems)
    if (schema.maxItems !== undefined) za = za.max(schema.maxItems)
    return za
  }
  if (schema.type === 'object') {
    const shape: Record<string, z.ZodType<any>> = {}
    if (schema.properties) {
      for (const key in schema.properties) {
        let propZod = jsonSchemaToZod(schema.properties[key])
        if (!schema.required || !schema.required.includes(key)) {
          propZod = propZod.optional()
        }
        shape[key] = propZod
      }
    }
    return schema.additionalProperties === false
      ? z.strictObject(shape)
      : z.object(shape)
  }
  return z.any()
}

/*  ------------------------------------------------------------------ */
/*  GitTool — exports a full main-project Tool directly                 */
/*  ------------------------------------------------------------------ */

const GIT_TOOL_INPUT_SCHEMA = {
  type: "object",
  properties: {
    commands: {
      type: "array",
      items: { type: "string", minLength: 1 },
      minItems: 1
    }
  },
  required: ["commands"],
  additionalProperties: false
} as const

/**
 * @type {Tool}
 */
export const GitTool = {
  name: 'GitTool',
  description: async () => 'Execute arbitrary git command(s) from commands array',
  listInterfaces: [
    { name: 'commands', purpose: '在 git 工作区内顺序执行命令序列' },
  ],
  prompt: async () => {
    const workDirBranchLines = await buildGitWorkDirBranchLines()
    const gitStatus = await buildGitStatusForPrompt()
    return `# GitTool policy
GitTool is the single execution entry for git workflows in this agent.

${workDirBranchLines}

Git status snapshot at tool-serialization time (gitWorkDir only, not session cwd):
${gitStatus}

Usage method:
- input must include "commands: string[]" only (no cwd field)
- ALL commands run exclusively in the configured git work directory (WS_GIT_WORK_DIR, default workspace/repo under agent root)
- commands may be full "git log ..." or bare subcommands like "log ..." / "status" (git prefix is added automatically)
- commands are executed sequentially in the work directory

When issuing multiple commands:
- If commands depend on each other, put them in one call via the commands array.
- If commands are independent, prefer separate tool calls in parallel.

Safety protocol: NEVER update git config, NEVER skip hooks unless user explicitly asks, NEVER force push main/master unless user explicitly asks.
Input is strict and fail-fast; no fallback behavior is applied.`
  },

  inputSchema: jsonSchemaToZod(GIT_TOOL_INPUT_SCHEMA),

  maxResultSizeChars: 100_000,

  isEnabled: () => true,

  /**
   * @param {ToolInput} input
   */
  isReadOnly: (input) => isReadOnlyCommands(input.commands),
  isConcurrencySafe: () => false,
  isDestructive: () => false,

  /**
   * @param {ToolInput} input
   * @param {ToolUseContext} _ctx
   * @returns {Promise<PermissionResult>}
   */
  checkPermissions: async (input, _ctx) => {
    return { behavior: 'ask', updatedInput: input }
  },

  /**
   * @param {ToolInput} input
   * @param {ToolUseContext} _ctx
   * @returns {Promise<InternalValidationResult>}
   */
  validateInput: async (input, _ctx) => {
    if (!input.commands || input.commands.length === 0) {
      return { result: false, message: 'commands array must not be empty', errorCode: 0 }
    }
    try {
      requireGitWorkDir()
      input.commands.forEach(cmd => normalizeGitCommand(cmd))
      return { result: true }
    } catch (err) {
      return { result: false, message: /** @type {Error} */(err).message, errorCode: 0 }
    }
  },

  /**
   * @param {ToolInput} input
   * @param {ToolUseContext} ctx
   * @returns {Promise<ToolResult<ToolOutput>>}
   */
  call: async (input, ctx) => {
    const steps: CommandSnapshot[] = []
    const gitWorkDir = requireGitWorkDir()
    const gitEnv = getGitCommandEnv()

    const toolResultsDir = ctx.toolResultsDir ?? join(gitWorkDir, '.ws-agents', 'tool-results')

    for (const rawCommand of input.commands) {
      const command = normalizeGitCommand(rawCommand)
      console.log(`[GitTool] Running: ${command}`)
      steps.push(
        await runCommand({
          cwd: gitWorkDir,
          command,
          signal: ctx.abortController.signal,
          env: gitEnv,
        }),
      )
    }
    const resultFile = await writeResultFile({
      toolResultsDir,
      payload: { action: 'git', cwd: gitWorkDir, steps },
    })

    return {
      data: { cwd: gitWorkDir, resultFile, steps }
    }
  },

  interruptBehavior: () => 'cancel',

  /**
   * @param {Partial<ToolInput>} _input
   * @returns {import('react').ReactNode}
   */
  renderToolUseMessage: (_input) => `Running GitTool`,

  /**
   * @param {ToolOutput} output
   * @returns {import('react').ReactNode}
   */
  renderToolResultMessage: (output) => {
    if (output && typeof output === 'object' && (output as any).isError) {
      return `Failed: ${(output as any).message}`
    }
    return `Completed GitTool`
  },

  /**
   * @param {ToolOutput} output
   * @param {string} toolUseID
   * @returns {import('@anthropic-ai/sdk/resources/index.mjs').ToolResultBlockParam}
   */
  mapToolResultToToolResultBlockParam: (output, toolUseID) => {
    const contentStr = typeof output === 'string' ? output : JSON.stringify(output, null, 2)
    if (output && typeof output === 'object' && (output as any).isError) {
      return {
        type: 'tool_result',
        tool_use_id: toolUseID,
        content: `<tool_use_error>${contentStr}</tool_use_error>`,
        is_error: true
      }
    }
    return {
      type: 'tool_result',
      tool_use_id: toolUseID,
      content: contentStr
    }
  },

  /**
   * @param {ToolInput} _input
   */
  toAutoClassifierInput: (_input) => '',
}

export default GitTool

export const _test = {
  normalizeGitCommand,
  GIT_SUBCOMMANDS,
  readGitBranchFromWorkDir,
  buildGitWorkDirBranchLines,
}
