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

/**
 * 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: Uses node-pty for PTY
 */
export class ClaudeUsageService {
  private claudeBinary = 'claude';
  private timeout = 30000; // 30 second timeout
  private isWindows = os.platform() === 'win32';

  /**
   * 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 platform-specific PTY implementation
   */
  private executeClaudeUsageCommand(): Promise<string> {
    if (this.isWindows) {
      return this.executeClaudeUsageCommandWindows();
    }
    return this.executeClaudeUsageCommandMac();
  }

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

      // Use a simple working directory (home or tmp)
      const workingDirectory = process.env.HOME || '/tmp';

      // Use 'expect' with an inline script to run claude /usage with a PTY
      // Wait for "Current session" header, then wait for full output before exiting
      const expectScript = `
        set timeout 20
        spawn claude /usage
        expect {
          "Current session" {
            sleep 2
            send "\\x1b"
          }
          "Esc to cancel" {
            sleep 3
            send "\\x1b"
          }
          timeout {}
          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 implementation using node-pty
   */
  private executeClaudeUsageCommandWindows(): Promise<string> {
    return new Promise((resolve, reject) => {
      let output = '';
      let settled = false;
      let hasSeenUsageData = false;

      const workingDirectory = process.env.USERPROFILE || os.homedir() || 'C:\\';

      const ptyProcess = pty.spawn('cmd.exe', ['/c', 'claude', '/usage'], {
        name: 'xterm-256color',
        cols: 120,
        rows: 30,
        cwd: workingDirectory,
        env: {
          ...process.env,
          TERM: 'xterm-256color',
        } as Record<string, string>,
      });

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

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

        // Check if we've seen the usage data (look for "Current session")
        if (!hasSeenUsageData && output.includes('Current session')) {
          hasSeenUsageData = true;
          // Wait for full output, then send escape to exit
          setTimeout(() => {
            if (!settled) {
              ptyProcess.write('\x1b'); // Send escape key
            }
          }, 2000);
        }

        // Fallback: if we see "Esc to cancel" but haven't seen usage data yet
        if (!hasSeenUsageData && output.includes('Esc to cancel')) {
          setTimeout(() => {
            if (!settled) {
              ptyProcess.write('\x1b'); // Send escape key
            }
          }, 3000);
        }
      });

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

        // Check for authentication errors in output
        if (output.includes('token_expired') || output.includes('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();
    }
  }
}
