import type { Command } from 'commander'
import { Effect } from 'effect'
import { GerritApiServiceLive } from '@/api/gerrit'
import { ConfigServiceLive } from '@/services/config'
import { CommitHookServiceLive } from '@/services/commit-hook'
import { registerStateCommands } from './register-state-commands'
import { rebaseCommand } from './commands/rebase'
import { submitCommand } from './commands/submit'
import { topicCommand, TOPIC_HELP_TEXT } from './commands/topic'
import { voteCommand } from './commands/vote'
import { projectsCommand } from './commands/projects'
import { buildStatusCommand, BUILD_STATUS_HELP_TEXT } from './commands/build-status'
import { checkoutCommand, CHECKOUT_HELP_TEXT } from './commands/checkout'
import { commentCommand, COMMENT_HELP_TEXT } from './commands/comment'
import { commentsCommand } from './commands/comments'
import { diffCommand } from './commands/diff'
import { extractUrlCommand } from './commands/extract-url'
import { installHookCommand } from './commands/install-hook'
import { openCommand } from './commands/open'
import { pushCommand, PUSH_HELP_TEXT } from './commands/push'
import { searchCommand, SEARCH_HELP_TEXT } from './commands/search'
import { setup } from './commands/setup'
import { showCommand, SHOW_HELP_TEXT } from './commands/show'
import { statusCommand } from './commands/status'
import { workspaceCommand } from './commands/workspace'
import { sanitizeCDATA } from '@/utils/shell-safety'
import { registerGroupCommands } from './register-group-commands'
import { registerReviewerCommands } from './register-reviewer-commands'
import { registerTreeCommands } from './register-tree-commands'
import { filesCommand } from './commands/files'
import { reviewersCommand } from './commands/reviewers'
import { retriggerCommand, RETRIGGER_HELP_TEXT } from './commands/retrigger'
import { cherryCommand, CHERRY_HELP_TEXT } from './commands/cherry'
import { registerListCommands } from './register-list-commands'
import { registerAnalyticsCommands } from './register-analytics-commands'

// Helper function to output error in plain text, JSON, or XML format
function outputError(
  error: unknown,
  options: { xml?: boolean; json?: boolean },
  resultTag: string,
): void {
  const errorMessage = error instanceof Error ? error.message : String(error)
  if (options.json) {
    console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
  } else if (options.xml) {
    console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
    console.log(`<${resultTag}>`)
    console.log(`  <status>error</status>`)
    console.log(`  <error><![CDATA[${errorMessage}]]></error>`)
    console.log(`</${resultTag}>`)
  } else {
    console.error('✗ Error:', errorMessage)
  }
}

// Helper function to execute Effect with standard error handling
async function executeEffect<E>(
  effect: Effect.Effect<void, E, never>,
  options: { xml?: boolean; json?: boolean },
  resultTag: string,
): Promise<void> {
  if (options.xml && options.json) {
    outputError(new Error('--xml and --json are mutually exclusive'), options, resultTag)
    process.exit(1)
  }
  try {
    await Effect.runPromise(effect)
  } catch (error) {
    outputError(error, options, resultTag)
    process.exit(1)
  }
}

export function registerCommands(program: Command): void {
  // setup command (new primary command)
  program
    .command('setup')
    .description('Configure Gerrit credentials and AI tools')
    .action(async () => {
      await setup()
    })

  // init command (kept for backward compatibility, redirects to setup)
  program
    .command('init')
    .description('Initialize Gerrit credentials (alias for setup)')
    .action(async () => {
      await setup()
    })

  // status command
  program
    .command('status')
    .description('Check connection status')
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .action(async (options) => {
      await executeEffect(
        statusCommand(options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        ),
        options,
        'status_result',
      )
    })

  // comment command
  program
    .command('comment <change-id>')
    .description('Post a comment on a change (accepts change number or Change-ID)')
    .option('-m, --message <message>', 'Comment message')
    .option('--file <file>', 'File path for line-specific comment (relative to repo root)')
    .option(
      '--line <line>',
      'Line number in the NEW version of the file (not diff line numbers)',
      parseInt,
    )
    .option(
      '--reply-to <comment-id>',
      'Reply to a comment thread (requires --file and --line; resolves thread by default)',
    )
    .option('--unresolved', 'Mark comment as unresolved (requires human attention)')
    .option('--batch', 'Read batch comments from stdin as JSON (see examples below)')
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .addHelpText('after', COMMENT_HELP_TEXT)
    .action(async (changeId, options) => {
      await executeEffect(
        commentCommand(changeId, options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        ),
        options,
        'comment_result',
      )
    })

  // diff command
  program
    .command('diff <change-id>')
    .description('Get diff for a change (accepts change number or Change-ID)')
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .option('--file <file>', 'Specific file to diff')
    .option('--files-only', 'List changed files only')
    .option('--format <format>', 'Output format (unified, json, files)')
    .action(async (changeId, options) => {
      await executeEffect(
        diffCommand(changeId, options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        ),
        options,
        'diff_result',
      )
    })

  registerListCommands(program)

  // search command
  program
    .command('search [query]')
    .description('Search changes using Gerrit query syntax')
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .option('-n, --limit <number>', 'Limit results (default: 25)')
    .addHelpText('after', SEARCH_HELP_TEXT)
    .action(async (query, options) => {
      const effect = searchCommand(query, options).pipe(
        Effect.provide(GerritApiServiceLive),
        Effect.provide(ConfigServiceLive),
      )
      await Effect.runPromise(effect).catch((error: unknown) => {
        const errorMessage = error instanceof Error ? error.message : String(error)
        if (options.json) {
          console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
        } else if (options.xml) {
          console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
          console.log(`<search_result>`)
          console.log(`  <status>error</status>`)
          console.log(`  <error><![CDATA[${errorMessage}]]></error>`)
          console.log(`</search_result>`)
        } else {
          console.error('✗ Error:', errorMessage)
        }
        process.exit(1)
      })
    })

  // workspace command (deprecated — use 'ger tree setup' instead)
  program
    .command('workspace <change-id>')
    .description('[deprecated: use "ger tree setup"] Create a git worktree for a Gerrit change')
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .action(async (changeId, options) => {
      if (!options.xml && !options.json) {
        console.error('Note: "ger workspace" is deprecated. Use "ger tree setup" instead.')
      }
      await executeEffect(
        workspaceCommand(changeId, options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        ),
        options,
        'workspace_result',
      )
    })

  registerTreeCommands(program)

  // abandon / restore / set-ready / set-wip commands
  registerStateCommands(program)

  // rebase command
  program
    .command('rebase [change-id]')
    .description('Rebase a change onto target branch (auto-detects from HEAD if not provided)')
    .option('--base <ref>', 'Base revision to rebase onto (default: target branch HEAD)')
    .option('--allow-conflicts', 'Allow rebasing even if conflicts exist')
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .action(async (changeId, options) => {
      await executeEffect(
        rebaseCommand(changeId, { ...options, allowConflicts: options.allowConflicts }).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        ),
        options,
        'rebase_result',
      )
    })

  // submit command
  program
    .command('submit <change-id>')
    .description('Submit a change for merging (accepts change number or Change-ID)')
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .action(async (changeId, options) => {
      await executeEffect(
        submitCommand(changeId, options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        ),
        options,
        'submit_result',
      )
    })

  // topic command
  program
    .command('topic [change-id] [topic]')
    .description('Get, set, or remove topic for a change (auto-detects from HEAD if not specified)')
    .option('--delete', 'Remove the topic from the change')
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .addHelpText('after', TOPIC_HELP_TEXT)
    .action(async (changeId, topic, options) => {
      await executeEffect(
        topicCommand(changeId, topic, options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        ),
        options,
        'topic_result',
      )
    })

  // vote command
  program
    .command('vote <change-id>')
    .description('Cast votes on a change (accepts change number or Change-ID)')
    .option('--code-review <value>', 'Code-Review vote (-2 to +2)', parseInt)
    .option('--verified <value>', 'Verified vote (-1 to +1)', parseInt)
    .option('--label <name> <value>', 'Custom label vote (can be used multiple times)')
    .option('-m, --message <message>', 'Comment with vote')
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .action(async (changeId, options) => {
      await executeEffect(
        voteCommand(changeId, options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        ),
        options,
        'vote_result',
      )
    })

  // Register all reviewer-related commands
  registerReviewerCommands(program)

  // projects command
  program
    .command('projects')
    .description('List Gerrit projects')
    .option('--pattern <regex>', 'Filter projects by name pattern')
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .action(async (options) => {
      await executeEffect(
        projectsCommand(options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        ),
        options,
        'projects_result',
      )
    })

  // Register all group-related commands
  registerGroupCommands(program)

  // retrigger command
  program
    .command('retrigger [change-id]')
    .description(
      'Post the CI retrigger comment on a change (auto-detects from HEAD if no change-id given)',
    )
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .addHelpText('after', RETRIGGER_HELP_TEXT)
    .action(async (changeId, options) => {
      await executeEffect(
        retriggerCommand(changeId as string | undefined, options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        ),
        options,
        'retrigger_result',
      )
    })

  // comments command
  program
    .command('comments <change-id>')
    .description(
      'Show all comments on a change with diff context (accepts change number or Change-ID)',
    )
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .action(async (changeId, options) => {
      await executeEffect(
        commentsCommand(changeId, options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        ),
        options,
        'comments_result',
      )
    })

  // open command
  program
    .command('open <change-id>')
    .description('Open a change in the browser (accepts change number or Change-ID)')
    .action(async (changeId, options) => {
      try {
        const effect = openCommand(changeId, options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        )
        await Effect.runPromise(effect)
      } catch (error) {
        console.error('✗ Error:', error instanceof Error ? error.message : String(error))
        process.exit(1)
      }
    })

  // show command
  program
    .command('show [change-id]')
    .description(
      'Show comprehensive change information (auto-detects from HEAD commit if not specified)',
    )
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .addHelpText('after', SHOW_HELP_TEXT)
    .action(async (changeId, options) => {
      try {
        const effect = showCommand(changeId, options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        )
        await Effect.runPromise(effect)
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error)
        if (options.json) {
          console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
        } else if (options.xml) {
          console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
          console.log(`<show_result>`)
          console.log(`  <status>error</status>`)
          console.log(`  <error><![CDATA[${sanitizeCDATA(errorMessage)}]]></error>`)
          console.log(`</show_result>`)
        } else {
          console.error('✗ Error:', errorMessage)
        }
        process.exit(1)
      }
    })

  // build-status command
  program
    .command('build-status [change-id]')
    .description(
      'Check build status from Gerrit messages (auto-detects from HEAD commit if not specified)',
    )
    .option('--watch', 'Watch build status until completion (mimics gh run watch)')
    .option('-i, --interval <seconds>', 'Refresh interval in seconds (default: 10)', '10')
    .option('--timeout <seconds>', 'Maximum wait time in seconds (default: 1800 / 30min)', '1800')
    .option('--exit-status', 'Exit with non-zero status if build fails')
    .addHelpText('after', BUILD_STATUS_HELP_TEXT)
    .action(async (changeId, cmdOptions) => {
      try {
        const effect = buildStatusCommand(changeId, {
          watch: cmdOptions.watch,
          interval: Number.parseInt(cmdOptions.interval, 10),
          timeout: Number.parseInt(cmdOptions.timeout, 10),
          exitStatus: cmdOptions.exitStatus,
        }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(ConfigServiceLive))
        await Effect.runPromise(effect)
      } catch (error) {
        // Errors are handled within the command itself
        // This catch is just for any unexpected errors
        if (error instanceof Error && error.message !== 'Process exited') {
          console.error('✗ Unexpected error:', error.message)
          process.exit(3)
        }
      }
    })

  // extract-url command
  program
    .command('extract-url <pattern> [change-id]')
    .description(
      'Extract URLs from change messages and comments (auto-detects from HEAD commit if not specified)',
    )
    .option('--include-comments', 'Also search inline comments (default: messages only)')
    .option('--regex', 'Treat pattern as regex instead of substring match')
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .addHelpText(
      'after',
      `
Examples:
  # Extract all Jenkins build-summary-report URLs (substring match)
  $ ger extract-url "build-summary-report"

  # Get the latest build URL using tail
  $ ger extract-url "build-summary-report" | tail -1

  # Get the first build URL using head
  $ ger extract-url "jenkins.inst-ci.net" | head -1

  # For a specific change (using change number)
  $ ger extract-url "build-summary" 391831

  # For a specific change (using Change-ID)
  $ ger extract-url "jenkins" If5a3ae8cb5a107e187447802358417f311d0c4b1

  # Use regex for precise matching
  $ ger extract-url "job/MyProject/job/main/\\d+/" --regex

  # Search both messages and inline comments
  $ ger extract-url "github.com" --include-comments

  # JSON output for scripting
  $ ger extract-url "jenkins" --json | jq -r '.urls[-1]'

  # XML output
  $ ger extract-url "jenkins" --xml

Note:
  - URLs are output in chronological order (oldest first)
  - Use tail -1 to get the latest URL, head -1 for the oldest
  - When no change-id is provided, it will be automatically extracted from the
    Change-ID footer in your HEAD commit`,
    )
    .action(async (pattern, changeId, options) => {
      try {
        const effect = extractUrlCommand(pattern, changeId, options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        )
        await Effect.runPromise(effect)
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error)
        if (options.json) {
          console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
        } else if (options.xml) {
          console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
          console.log(`<extract_url_result>`)
          console.log(`  <status>error</status>`)
          console.log(`  <error><![CDATA[${sanitizeCDATA(errorMessage)}]]></error>`)
          console.log(`</extract_url_result>`)
        } else {
          console.error('✗ Error:', errorMessage)
        }
        process.exit(1)
      }
    })

  // install-hook command
  program
    .command('install-hook')
    .description('Install the Gerrit commit-msg hook for automatic Change-Id generation')
    .option('--force', 'Overwrite existing hook')
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .addHelpText(
      'after',
      `
Examples:
  # Install the commit-msg hook
  $ ger install-hook

  # Force reinstall (overwrite existing)
  $ ger install-hook --force

Note:
  - Downloads hook from your configured Gerrit server
  - Installs to .git/hooks/commit-msg
  - Makes hook executable (chmod +x)
  - Required for commits to have Change-Id footers`,
    )
    .action(async (options) => {
      await executeEffect(
        installHookCommand(options).pipe(
          Effect.provide(CommitHookServiceLive),
          Effect.provide(ConfigServiceLive),
        ),
        options,
        'install_hook_result',
      )
    })

  // push command
  program
    .command('push')
    .description('Push commits to Gerrit for code review')
    .option('-b, --branch <branch>', 'Target branch (default: auto-detect)')
    .option('-t, --topic <topic>', 'Set change topic')
    .option('-r, --reviewer <email...>', 'Add reviewer(s)')
    .option('--cc <email...>', 'Add CC recipient(s)')
    .option('--wip', 'Mark as work-in-progress')
    .option('--ready', 'Mark as ready for review')
    .option('--hashtag <tag...>', 'Add hashtag(s)')
    .option('--private', 'Mark change as private')
    .option('--draft', 'Alias for --wip')
    .option('--dry-run', 'Show what would be pushed without pushing')
    .addHelpText('after', PUSH_HELP_TEXT)
    .action(async (options) => {
      try {
        const effect = pushCommand({
          branch: options.branch,
          topic: options.topic,
          reviewer: options.reviewer,
          cc: options.cc,
          wip: options.wip,
          ready: options.ready,
          hashtag: options.hashtag,
          private: options.private,
          draft: options.draft,
          dryRun: options.dryRun,
        }).pipe(Effect.provide(CommitHookServiceLive), Effect.provide(ConfigServiceLive))
        await Effect.runPromise(effect)
      } catch (error) {
        console.error('Error:', error instanceof Error ? error.message : String(error))
        process.exit(1)
      }
    })

  // files command
  program
    .command('files [change-id]')
    .description(
      'List files changed in a Gerrit change (auto-detects from HEAD commit if not specified)',
    )
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .action(async (changeId, options) => {
      await executeEffect(
        filesCommand(changeId, options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        ),
        options,
        'files_result',
      )
    })

  // reviewers command
  program
    .command('reviewers [change-id]')
    .description(
      'List reviewers on a Gerrit change (auto-detects from HEAD commit if not specified)',
    )
    .option('--xml', 'XML output for LLM consumption')
    .option('--json', 'JSON output for programmatic consumption')
    .action(async (changeId, options) => {
      await executeEffect(
        reviewersCommand(changeId, options).pipe(
          Effect.provide(GerritApiServiceLive),
          Effect.provide(ConfigServiceLive),
        ),
        options,
        'reviewers_result',
      )
    })

  // checkout command
  program
    .command('checkout <change-id>')
    .description('Fetch and checkout a Gerrit change')
    .option('--detach', 'Checkout as detached HEAD without creating branch')
    .option('--remote <name>', 'Use specific git remote (default: auto-detect)')
    .addHelpText('after', CHECKOUT_HELP_TEXT)
    .action(async (changeId, options) => {
      try {
        const effect = checkoutCommand(changeId, {
          detach: options.detach,
          remote: options.remote,
        }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(ConfigServiceLive))
        await Effect.runPromise(effect)
      } catch (error) {
        console.error('Error:', error instanceof Error ? error.message : String(error))
        process.exit(1)
      }
    })

  registerAnalyticsCommands(program)

  // cherry command
  program
    .command('cherry <change-id>')
    .description('Fetch and cherry-pick a Gerrit change onto the current branch')
    .option('-n, --no-commit', 'Stage changes without committing')
    .option('--no-verify', 'Bypass git commit hooks during cherry-pick')
    .option('--remote <name>', 'Use specific git remote (default: auto-detect)')
    .addHelpText('after', CHERRY_HELP_TEXT)
    .action(async (changeId, options) => {
      await executeEffect(
        cherryCommand(changeId, {
          noCommit: options.noCommit,
          noVerify: options.noVerify,
          remote: options.remote,
        }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(ConfigServiceLive)),
        options,
        'cherry_result',
      )
    })
}
