import { spawn } from 'child_process';
import * as os from 'os';
import * as pty from 'node-pty';
import { ClaudeUsage } from '../routes/claude/types.js';
import { createLogger } from '@automaker/utils';

/**
 * Claude Usage Service
 *
 * Fetches usage data by executing the Claude CLI's /usage command.
 * This approach doesn't require any API keys - it relies on the user
 * having already authenticated via `claude login`.
 *
 * Platform-specific implementations:
 * - macOS: Uses 'expect' command for PTY
 * - Windows/Linux: Uses node-pty for PTY
 */
const logger = createLogger('ClaudeUsage');

export class ClaudeUsageService {
  private claudeBinary = 'claude';
  private timeout = 30000; // 30 second timeout
  private isWindows = os.platform() === 'win32';
  private isLinux = os.platform() === 'linux';
  // On Windows, ConPTY requires AttachConsole which fails in Electron/service mode
  // Detect Electron by checking for electron-specific env vars or process properties
  // When in Electron, always use winpty to avoid ConPTY's AttachConsole errors
  private isElectron =
    !!(process.versions && (process.versions as Record<string, string>).electron) ||
    !!process.env.ELECTRON_RUN_AS_NODE;
  private useConptyFallback = false; // Track if we need to use winpty fallback on Windows

  /**
   * Kill a PTY process with platform-specific handling.
   * Windows doesn't support Unix signals like SIGTERM, so we call kill() without arguments.
   * On Unix-like systems (macOS, Linux), we can specify the signal.
   *
   * @param ptyProcess - The PTY process to kill
   * @param signal - The signal to send on Unix-like systems (default: 'SIGTERM')
   */
  private killPtyProcess(ptyProcess: pty.IPty, signal: string = 'SIGTERM'): void {
    if (this.isWindows) {
      ptyProcess.kill();
    } else {
      ptyProcess.kill(signal);
    }
  }

  /**
   * Check if Claude CLI is available on the system
   */
  async isAvailable(): Promise<boolean> {
    return new Promise((resolve) => {
      const checkCmd = this.isWindows ? 'where' : 'which';
      const proc = spawn(checkCmd, [this.claudeBinary]);
      proc.on('close', (code) => {
        resolve(code === 0);
      });
      proc.on('error', () => {
        resolve(false);
      });
    });
  }

  /**
   * Fetch usage data by executing the Claude CLI
   */
  async fetchUsageData(): Promise<ClaudeUsage> {
    const output = await this.executeClaudeUsageCommand();
    return this.parseUsageOutput(output);
  }

  /**
   * Execute the claude /usage command and return the output
   * Uses node-pty on all platforms for consistency
   */
  private executeClaudeUsageCommand(): Promise<string> {
    // Use node-pty on all platforms - it's more reliable than expect on macOS
    return this.executeClaudeUsageCommandPty();
  }

  /**
   * macOS implementation using 'expect' command
   */
  private executeClaudeUsageCommandMac(): Promise<string> {
    return new Promise((resolve, reject) => {
      let stdout = '';
      let stderr = '';
      let settled = false;

      // Use current working directory - likely already trusted by Claude CLI
      const workingDirectory = process.cwd();

      // Use 'expect' with an inline script to run claude /usage with a PTY
      // Running from cwd which should already be trusted
      const expectScript = `
        set timeout 30
        spawn claude /usage

        # Wait for usage data or handle trust prompt if needed
        expect {
          -re "Ready to code|permission to work|Do you want to work" {
            # Trust prompt appeared - send Enter to approve
            sleep 1
            send "\\r"
            exp_continue
          }
          "Current session" {
            # Usage data appeared - wait for full output, then exit
            sleep 3
            send "\\x1b"
          }
          "% left" {
            # Usage percentage appeared
            sleep 3
            send "\\x1b"
          }
          timeout {
            send "\\x1b"
          }
          eof {}
        }
        expect eof
      `;

      const proc = spawn('expect', ['-c', expectScript], {
        cwd: workingDirectory,
        env: {
          ...process.env,
          TERM: 'xterm-256color',
        },
      });

      const timeoutId = setTimeout(() => {
        if (!settled) {
          settled = true;
          proc.kill();
          reject(new Error('Command timed out'));
        }
      }, this.timeout);

      proc.stdout.on('data', (data) => {
        stdout += data.toString();
      });

      proc.stderr.on('data', (data) => {
        stderr += data.toString();
      });

      proc.on('close', (code) => {
        clearTimeout(timeoutId);
        if (settled) return;
        settled = true;

        // Check for authentication errors in output
        if (
          stdout.includes('token_expired') ||
          stdout.includes('authentication_error') ||
          stderr.includes('token_expired') ||
          stderr.includes('authentication_error')
        ) {
          reject(new Error("Authentication required - please run 'claude login'"));
          return;
        }

        // Even if exit code is non-zero, we might have useful output
        if (stdout.trim()) {
          resolve(stdout);
        } else if (code !== 0) {
          reject(new Error(stderr || `Command exited with code ${code}`));
        } else {
          reject(new Error('No output from claude command'));
        }
      });

      proc.on('error', (err) => {
        clearTimeout(timeoutId);
        if (!settled) {
          settled = true;
          reject(new Error(`Failed to execute claude: ${err.message}`));
        }
      });
    });
  }

  /**
   * Windows/Linux implementation using node-pty
   */
  private executeClaudeUsageCommandPty(): Promise<string> {
    return new Promise((resolve, reject) => {
      let output = '';
      let settled = false;
      let hasSeenUsageData = false;
      let hasSeenTrustPrompt = false;

      // Use current working directory (project dir) - most likely already trusted by Claude CLI
      const workingDirectory = process.cwd();

      // Use platform-appropriate shell and command
      const shell = this.isWindows ? 'cmd.exe' : '/bin/sh';
      // Use --add-dir to whitelist the current directory and bypass the trust prompt
      // We don't pass /usage here, we'll type it into the REPL
      const args = this.isWindows
        ? ['/c', 'claude', '--add-dir', workingDirectory]
        : ['-c', `claude --add-dir "${workingDirectory}"`];

      // Using 'any' for ptyProcess because node-pty types don't include 'killed' property
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      let ptyProcess: any = null;

      // Build PTY spawn options
      const ptyOptions: pty.IPtyForkOptions = {
        name: 'xterm-256color',
        cols: 120,
        rows: 30,
        cwd: workingDirectory,
        env: {
          ...process.env,
          TERM: 'xterm-256color',
        } as Record<string, string>,
      };

      // On Windows, always use winpty instead of ConPTY
      // ConPTY requires AttachConsole which fails in many contexts:
      // - Electron apps without a console
      // - VS Code integrated terminal
      // - Spawned from other applications
      // The error happens in a subprocess so we can't catch it - must proactively disable
      if (this.isWindows) {
        (ptyOptions as pty.IWindowsPtyForkOptions).useConpty = false;
        logger.info(
          '[executeClaudeUsageCommandPty] Using winpty on Windows (ConPTY disabled for compatibility)'
        );
      }

      try {
        ptyProcess = pty.spawn(shell, args, ptyOptions);
      } catch (spawnError) {
        const errorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);

        // Check for Windows ConPTY-specific errors
        if (this.isWindows && errorMessage.includes('AttachConsole failed')) {
          // ConPTY failed - try winpty fallback
          if (!this.useConptyFallback) {
            logger.warn(
              '[executeClaudeUsageCommandPty] ConPTY AttachConsole failed, retrying with winpty fallback'
            );
            this.useConptyFallback = true;

            try {
              (ptyOptions as pty.IWindowsPtyForkOptions).useConpty = false;
              ptyProcess = pty.spawn(shell, args, ptyOptions);
              logger.info(
                '[executeClaudeUsageCommandPty] Successfully spawned with winpty fallback'
              );
            } catch (fallbackError) {
              const fallbackMessage =
                fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
              logger.error(
                '[executeClaudeUsageCommandPty] Winpty fallback also failed:',
                fallbackMessage
              );
              reject(
                new Error(
                  `Windows PTY unavailable: Both ConPTY and winpty failed. This typically happens when running in Electron without a console. ConPTY error: ${errorMessage}. Winpty error: ${fallbackMessage}`
                )
              );
              return;
            }
          } else {
            logger.error('[executeClaudeUsageCommandPty] Winpty fallback failed:', errorMessage);
            reject(
              new Error(
                `Windows PTY unavailable: ${errorMessage}. The application is running without console access (common in Electron). Try running from a terminal window.`
              )
            );
            return;
          }
        } else {
          logger.error('[executeClaudeUsageCommandPty] Failed to spawn PTY:', errorMessage);
          reject(
            new Error(
              `Unable to access terminal: ${errorMessage}. Claude CLI may not be available or PTY support is limited in this environment.`
            )
          );
          return;
        }
      }

      const timeoutId = setTimeout(() => {
        if (!settled) {
          settled = true;
          if (ptyProcess && !ptyProcess.killed) {
            this.killPtyProcess(ptyProcess);
          }
          // Don't fail if we have data - return it instead
          if (output.includes('Current session')) {
            resolve(output);
          } else if (hasSeenTrustPrompt) {
            // Trust prompt was shown but we couldn't auto-approve it
            reject(
              new Error(
                'TRUST_PROMPT_PENDING: Claude CLI is waiting for folder permission. Please run "claude" in your terminal and approve access to continue.'
              )
            );
          } else {
            reject(
              new Error(
                'The Claude CLI took too long to respond. This can happen if the CLI is waiting for a trust prompt or is otherwise busy.'
              )
            );
          }
        }
      }, 45000); // 45 second timeout

      let hasSentCommand = false;
      let hasApprovedTrust = false;

      ptyProcess.onData((data: string) => {
        output += data;

        // Strip ANSI codes for easier matching
        // eslint-disable-next-line no-control-regex
        const cleanOutput = output.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');

        // Check for specific authentication/permission errors
        // Must be very specific to avoid false positives from garbled terminal encoding
        // Removed permission_error check as it was causing false positives with winpty encoding
        const authChecks = {
          oauth: cleanOutput.includes('OAuth token does not meet scope requirement'),
          tokenExpired: cleanOutput.includes('token_expired'),
          // Only match if it looks like a JSON API error response
          authError:
            cleanOutput.includes('"type":"authentication_error"') ||
            cleanOutput.includes('"type": "authentication_error"'),
        };
        const hasAuthError = authChecks.oauth || authChecks.tokenExpired || authChecks.authError;

        if (hasAuthError) {
          if (!settled) {
            settled = true;
            if (ptyProcess && !ptyProcess.killed) {
              this.killPtyProcess(ptyProcess);
            }
            reject(
              new Error(
                "Claude CLI authentication issue. Please run 'claude logout' and then 'claude login' in your terminal to refresh permissions."
              )
            );
          }
          return;
        }

        // Check if we've seen the usage data (look for "Current session" or the TUI Usage header)
        // Also check for percentage patterns that appear in usage output
        const hasUsageIndicators =
          cleanOutput.includes('Current session') ||
          (cleanOutput.includes('Usage') && cleanOutput.includes('% left')) ||
          // Additional patterns for winpty - look for percentage patterns
          /\d+%\s*(left|used|remaining)/i.test(cleanOutput) ||
          cleanOutput.includes('Resets in') ||
          cleanOutput.includes('Current week');

        if (!hasSeenUsageData && hasUsageIndicators) {
          hasSeenUsageData = true;
          // Wait for full output, then send escape to exit
          setTimeout(() => {
            if (!settled && ptyProcess && !ptyProcess.killed) {
              ptyProcess.write('\x1b'); // Send escape key

              // Fallback: if ESC doesn't exit (Linux), use SIGTERM after 2s
              // Windows doesn't support signals, so killPtyProcess handles platform differences
              setTimeout(() => {
                if (!settled && ptyProcess && !ptyProcess.killed) {
                  this.killPtyProcess(ptyProcess);
                }
              }, 2000);
            }
          }, 3000);
        }

        // Handle Trust Dialog - multiple variants:
        // - "Do you want to work in this folder?"
        // - "Ready to code here?" / "I'll need permission to work with your files"
        // Since we are running in cwd (project dir), it is safe to approve.
        if (
          !hasApprovedTrust &&
          (cleanOutput.includes('Do you want to work in this folder?') ||
            cleanOutput.includes('Ready to code here') ||
            cleanOutput.includes('permission to work with your files'))
        ) {
          hasApprovedTrust = true;
          hasSeenTrustPrompt = true;
          // Wait a tiny bit to ensure prompt is ready, then send Enter
          setTimeout(() => {
            if (!settled && ptyProcess && !ptyProcess.killed) {
              ptyProcess.write('\r');
            }
          }, 1000);
        }

        // Detect REPL prompt and send /usage command
        // On Windows with winpty, Unicode prompt char ❯ gets garbled, so also check for ASCII indicators
        const isReplReady =
          cleanOutput.includes('❯') ||
          cleanOutput.includes('? for shortcuts') ||
          // Fallback for winpty garbled encoding - detect CLI welcome screen elements
          (cleanOutput.includes('Welcome back') && cleanOutput.includes('Claude')) ||
          (cleanOutput.includes('Tips for getting started') && cleanOutput.includes('Claude')) ||
          // Detect model indicator which appears when REPL is ready
          (cleanOutput.includes('Opus') && cleanOutput.includes('Claude API')) ||
          (cleanOutput.includes('Sonnet') && cleanOutput.includes('Claude API'));

        if (!hasSentCommand && isReplReady) {
          hasSentCommand = true;
          // Wait for REPL to fully settle
          setTimeout(() => {
            if (!settled && ptyProcess && !ptyProcess.killed) {
              // Send command with carriage return
              ptyProcess.write('/usage\r');

              // Send another enter after 1 second to confirm selection if autocomplete menu appeared
              setTimeout(() => {
                if (!settled && ptyProcess && !ptyProcess.killed) {
                  ptyProcess.write('\r');
                }
              }, 1200);
            }
          }, 1500);
        }

        // Fallback: if we see "Esc to cancel" but haven't seen usage data yet
        if (
          !hasSeenUsageData &&
          cleanOutput.includes('Esc to cancel') &&
          !cleanOutput.includes('Do you want to work in this folder?')
        ) {
          setTimeout(() => {
            if (!settled && ptyProcess && !ptyProcess.killed) {
              ptyProcess.write('\x1b'); // Send escape key
            }
          }, 5000);
        }
      });

      ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
        clearTimeout(timeoutId);
        if (settled) return;
        settled = true;

        // Check for auth errors - must be specific to avoid false positives
        // Removed permission_error check as it was causing false positives with winpty encoding
        if (output.includes('token_expired') || output.includes('"type":"authentication_error"')) {
          reject(new Error("Authentication required - please run 'claude login'"));
          return;
        }

        if (output.trim()) {
          resolve(output);
        } else if (exitCode !== 0) {
          reject(new Error(`Command exited with code ${exitCode}`));
        } else {
          reject(new Error('No output from claude command'));
        }
      });
    });
  }

  /**
   * Strip ANSI escape codes from text
   */
  private stripAnsiCodes(text: string): string {
    // eslint-disable-next-line no-control-regex
    return text.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
  }

  /**
   * Parse the Claude CLI output to extract usage information
   *
   * Expected output format:
   * ```
   * Claude Code v1.0.27
   *
   * Current session
   * ████████████████░░░░ 65% left
   * Resets in 2h 15m
   *
   * Current week (all models)
   * ██████████░░░░░░░░░░ 35% left
   * Resets Jan 15, 3:30pm (America/Los_Angeles)
   *
   * Current week (Opus)
   * ████████████████████ 80% left
   * Resets Jan 15, 3:30pm (America/Los_Angeles)
   * ```
   */
  private parseUsageOutput(rawOutput: string): ClaudeUsage {
    const output = this.stripAnsiCodes(rawOutput);
    const lines = output
      .split('\n')
      .map((l) => l.trim())
      .filter((l) => l);

    // Parse session usage
    const sessionData = this.parseSection(lines, 'Current session', 'session');

    // Parse weekly usage (all models)
    const weeklyData = this.parseSection(lines, 'Current week (all models)', 'weekly');

    // Parse Sonnet/Opus usage - try different labels
    let sonnetData = this.parseSection(lines, 'Current week (Sonnet only)', 'sonnet');
    if (sonnetData.percentage === 0) {
      sonnetData = this.parseSection(lines, 'Current week (Sonnet)', 'sonnet');
    }
    if (sonnetData.percentage === 0) {
      sonnetData = this.parseSection(lines, 'Current week (Opus)', 'sonnet');
    }

    return {
      sessionTokensUsed: 0, // Not available from CLI
      sessionLimit: 0, // Not available from CLI
      sessionPercentage: sessionData.percentage,
      sessionResetTime: sessionData.resetTime,
      sessionResetText: sessionData.resetText,

      weeklyTokensUsed: 0, // Not available from CLI
      weeklyLimit: 0, // Not available from CLI
      weeklyPercentage: weeklyData.percentage,
      weeklyResetTime: weeklyData.resetTime,
      weeklyResetText: weeklyData.resetText,

      sonnetWeeklyTokensUsed: 0, // Not available from CLI
      sonnetWeeklyPercentage: sonnetData.percentage,
      sonnetResetText: sonnetData.resetText,

      costUsed: null, // Not available from CLI
      costLimit: null,
      costCurrency: null,

      lastUpdated: new Date().toISOString(),
      userTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    };
  }

  /**
   * Parse a section of the usage output to extract percentage and reset time
   */
  private parseSection(
    lines: string[],
    sectionLabel: string,
    type: string
  ): { percentage: number; resetTime: string; resetText: string } {
    let percentage = 0;
    let resetTime = this.getDefaultResetTime(type);
    let resetText = '';

    // Find the LAST occurrence of the section (terminal output has multiple screen refreshes)
    let sectionIndex = -1;
    for (let i = lines.length - 1; i >= 0; i--) {
      if (lines[i].toLowerCase().includes(sectionLabel.toLowerCase())) {
        sectionIndex = i;
        break;
      }
    }

    if (sectionIndex === -1) {
      return { percentage, resetTime, resetText };
    }

    // Look at the lines following the section header (within a window of 5 lines)
    const searchWindow = lines.slice(sectionIndex, sectionIndex + 5);

    for (const line of searchWindow) {
      // Extract percentage - only take the first match (avoid picking up next section's data)
      if (percentage === 0) {
        const percentMatch = line.match(/(\d{1,3})\s*%\s*(left|used|remaining)/i);
        if (percentMatch) {
          const value = parseInt(percentMatch[1], 10);
          const isUsed = percentMatch[2].toLowerCase() === 'used';
          // Convert "left" to "used" percentage (our UI shows % used)
          percentage = isUsed ? value : 100 - value;
        }
      }

      // Extract reset time - only take the first match
      if (!resetText && line.toLowerCase().includes('reset')) {
        resetText = line;
      }
    }

    // Parse the reset time if we found one
    if (resetText) {
      resetTime = this.parseResetTime(resetText, type);
      // Strip timezone like "(Asia/Dubai)" from the display text
      resetText = resetText.replace(/\s*\([A-Za-z_\/]+\)\s*$/, '').trim();
    }

    return { percentage, resetTime, resetText };
  }

  /**
   * Parse reset time from text like "Resets in 2h 15m", "Resets 11am", or "Resets Dec 22 at 8pm"
   */
  private parseResetTime(text: string, type: string): string {
    const now = new Date();

    // Try to parse duration format: "Resets in 2h 15m" or "Resets in 30m"
    const durationMatch = text.match(
      /(\d+)\s*h(?:ours?)?(?:\s+(\d+)\s*m(?:in)?)?|(\d+)\s*m(?:in)?/i
    );
    if (durationMatch) {
      let hours = 0;
      let minutes = 0;

      if (durationMatch[1]) {
        hours = parseInt(durationMatch[1], 10);
        minutes = durationMatch[2] ? parseInt(durationMatch[2], 10) : 0;
      } else if (durationMatch[3]) {
        minutes = parseInt(durationMatch[3], 10);
      }

      const resetDate = new Date(now.getTime() + (hours * 60 + minutes) * 60 * 1000);
      return resetDate.toISOString();
    }

    // Try to parse simple time-only format: "Resets 11am" or "Resets 3pm"
    const simpleTimeMatch = text.match(/resets\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i);
    if (simpleTimeMatch) {
      let hours = parseInt(simpleTimeMatch[1], 10);
      const minutes = simpleTimeMatch[2] ? parseInt(simpleTimeMatch[2], 10) : 0;
      const ampm = simpleTimeMatch[3].toLowerCase();

      // Convert 12-hour to 24-hour
      if (ampm === 'pm' && hours !== 12) {
        hours += 12;
      } else if (ampm === 'am' && hours === 12) {
        hours = 0;
      }

      // Create date for today at specified time
      const resetDate = new Date(now);
      resetDate.setHours(hours, minutes, 0, 0);

      // If time has passed, use tomorrow
      if (resetDate <= now) {
        resetDate.setDate(resetDate.getDate() + 1);
      }
      return resetDate.toISOString();
    }

    // Try to parse date format: "Resets Dec 22 at 8pm" or "Resets Jan 15, 3:30pm"
    const dateMatch = text.match(
      /([A-Za-z]{3,})\s+(\d{1,2})(?:\s+at\s+|\s*,?\s*)(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i
    );
    if (dateMatch) {
      const monthName = dateMatch[1];
      const day = parseInt(dateMatch[2], 10);
      let hours = parseInt(dateMatch[3], 10);
      const minutes = dateMatch[4] ? parseInt(dateMatch[4], 10) : 0;
      const ampm = dateMatch[5].toLowerCase();

      // Convert 12-hour to 24-hour
      if (ampm === 'pm' && hours !== 12) {
        hours += 12;
      } else if (ampm === 'am' && hours === 12) {
        hours = 0;
      }

      // Parse month name
      const months: Record<string, number> = {
        jan: 0,
        feb: 1,
        mar: 2,
        apr: 3,
        may: 4,
        jun: 5,
        jul: 6,
        aug: 7,
        sep: 8,
        oct: 9,
        nov: 10,
        dec: 11,
      };
      const month = months[monthName.toLowerCase().substring(0, 3)];

      if (month !== undefined) {
        let year = now.getFullYear();
        // If the date appears to be in the past, assume next year
        const resetDate = new Date(year, month, day, hours, minutes);
        if (resetDate < now) {
          resetDate.setFullYear(year + 1);
        }
        return resetDate.toISOString();
      }
    }

    // Fallback to default
    return this.getDefaultResetTime(type);
  }

  /**
   * Get default reset time based on usage type
   */
  private getDefaultResetTime(type: string): string {
    const now = new Date();

    if (type === 'session') {
      // Session resets in ~5 hours
      return new Date(now.getTime() + 5 * 60 * 60 * 1000).toISOString();
    } else {
      // Weekly resets on next Monday around noon
      const result = new Date(now);
      const currentDay = now.getDay();
      let daysUntilMonday = (1 + 7 - currentDay) % 7;
      if (daysUntilMonday === 0) daysUntilMonday = 7;
      result.setDate(result.getDate() + daysUntilMonday);
      result.setHours(12, 59, 0, 0);
      return result.toISOString();
    }
  }
}
