import { execFile } from 'child_process';
import { existsSync, statSync } from 'fs';
import os from 'os';
import path from 'path';
import { logger } from '../utils/utils';
import type SettingsManager from './settings-manager';
import type { FileOpenerDirectoryEntry } from '../types';

interface OpenFileResult {
  success: boolean;
  error?: string;
}

interface OpenFileOptions {
  lineNumber?: number;
  columnNumber?: number;
}

// Editor configuration for line number support
interface EditorConfig {
  // Use 'open -na <appName> --args' to launch (most editors)
  useOpenArgs?: boolean;
  // CLI command for editors that require their own CLI tool (e.g. xed for Xcode)
  cli?: string;
  // Format for line number argument: 'goto' = --goto file:line:col, 'line' = -l N file, 'colon' = file:line:col
  // Default (undefined) = JetBrains style: --line N file
  lineFormat?: 'goto' | 'line' | 'colon';
}

// Known editors and their configurations
// All editors use 'open -na <app> --args ...' for reliable macOS integration without CLI dependency
const EDITOR_CONFIGS: Record<string, EditorConfig> = {
  // VSCode and variants (--goto file:line:column)
  'Visual Studio Code': { useOpenArgs: true, lineFormat: 'goto' },
  'Visual Studio Code - Insiders': { useOpenArgs: true, lineFormat: 'goto' },
  'VSCodium': { useOpenArgs: true, lineFormat: 'goto' },
  'Cursor': { useOpenArgs: true, lineFormat: 'goto' },
  'Windsurf': { useOpenArgs: true, lineFormat: 'goto' },
  'Antigravity': { useOpenArgs: true, lineFormat: 'goto' },
  'Kiro': { useOpenArgs: true, lineFormat: 'goto' },
  // JetBrains IDEs (--line N file)
  'IntelliJ IDEA': { useOpenArgs: true },
  'IntelliJ IDEA Ultimate': { useOpenArgs: true },
  'IntelliJ IDEA Community': { useOpenArgs: true },
  'WebStorm': { useOpenArgs: true },
  'PyCharm': { useOpenArgs: true },
  'PyCharm Professional': { useOpenArgs: true },
  'PyCharm Community': { useOpenArgs: true },
  'GoLand': { useOpenArgs: true },
  'RubyMine': { useOpenArgs: true },
  'PhpStorm': { useOpenArgs: true },
  'CLion': { useOpenArgs: true },
  'Rider': { useOpenArgs: true },
  'DataGrip': { useOpenArgs: true },
  'AppCode': { useOpenArgs: true },
  'Android Studio': { useOpenArgs: true },
  // Apple (xed CLI required - falls back to 'open' if xed unavailable)
  'Xcode': { cli: 'xed', lineFormat: 'line' },
  // Other editors
  'Sublime Text': { useOpenArgs: true, lineFormat: 'colon' },
  'TextMate': { useOpenArgs: true, lineFormat: 'line' },
  'Atom': { useOpenArgs: true, lineFormat: 'colon' },
  'Zed': { useOpenArgs: true, lineFormat: 'colon' },
};

/**
 * Find project root by traversing parent directories looking for .git
 * Works with both regular repos (.git directory) and worktrees (.git file)
 */
function findProjectRoot(filePath: string): string | null {
  let dir = path.dirname(filePath);
  let parent = path.dirname(dir);
  while (dir !== parent) {
    if (existsSync(path.join(dir, '.git'))) {
      return dir;
    }
    dir = parent;
    parent = path.dirname(dir);
  }
  return null;
}

/**
 * Case-insensitive lookup for editor config
 * Returns both the config and the canonical name from EDITOR_CONFIGS
 */
function findEditorConfig(appName: string): { config: EditorConfig; canonicalName: string } | null {
  // Try exact match first
  if (EDITOR_CONFIGS[appName]) {
    return { config: EDITOR_CONFIGS[appName], canonicalName: appName };
  }

  // Case-insensitive lookup
  const lowerAppName = appName.toLowerCase();
  for (const [key, config] of Object.entries(EDITOR_CONFIGS)) {
    if (key.toLowerCase() === lowerAppName) {
      return { config, canonicalName: key };
    }
  }

  return null;
}

export class FileOpenerManager {
  private settingsManager: SettingsManager;

  constructor(settingsManager: SettingsManager) {
    this.settingsManager = settingsManager;
  }

  /**
   * ファイルを適切なアプリで開く
   * @param filePath ファイルパス
   * @param options オプション（行番号など）
   */
  async openFile(filePath: string, options?: OpenFileOptions): Promise<OpenFileResult> {
    const settings = this.settingsManager.getSettings();
    const ext = path.extname(filePath).slice(1).toLowerCase();

    // 拡張子に対応するアプリを取得（最優先）
    // 次にディレクトリ設定、最後にグローバルdefaultEditor
    const app = settings.fileOpener?.extensions?.[ext]
                || this.findDirectoryEditor(filePath, settings.fileOpener?.directories)
                || settings.fileOpener?.defaultEditor;

    if (app) {
      return this.openWithApp(filePath, app, options);
    }

    // 行番号が指定されていてエディタ未設定の場合、デフォルトアプリを検出して行ジャンプを試みる
    if (options?.lineNumber) {
      try {
        const detectedApp = await this.detectDefaultApp(filePath);
        if (detectedApp) {
          const editorMatch = findEditorConfig(detectedApp);
          if (editorMatch) {
            logger.info('Detected default app for line number support', { detectedApp, filePath });
            return this.openWithApp(filePath, editorMatch.canonicalName, options);
          }
        }
      } catch {
        // Detection failed, fall through to default
      }
    }

    // デフォルト動作（システムデフォルトアプリ）
    return this.openWithDefault(filePath);
  }

  /**
   * 指定アプリでファイルを開く（execFileを使用 - シェルインジェクション防止）
   */
  private openWithApp(filePath: string, appName: string, options?: OpenFileOptions): Promise<OpenFileResult> {
    return new Promise((resolve) => {
      // アプリ名の検証
      if (!appName || typeof appName !== 'string') {
        logger.warn('Invalid app name provided', { appName });
        this.openWithDefault(filePath).then(resolve);
        return;
      }

      // 基本的なパストラバーサルパターンの検出
      // ユーザー設定のアプリは信頼するが、明らかな攻撃パターンは防ぐ
      if (appName.includes('..') || appName.includes('/') || appName.includes('\0')) {
        logger.warn('Potentially malicious app name detected', { appName });
        this.openWithDefault(filePath).then(resolve);
        return;
      }

      // Detect project root (.git) once for all code paths
      const projectRoot = findProjectRoot(filePath);
      if (projectRoot) {
        logger.info('Detected project root for file opener', { projectRoot, appName, filePath });
      }

      // Check if we have a line number and the editor supports it
      // Use case-insensitive lookup for editor config
      const editorMatch = findEditorConfig(appName);
      if (options?.lineNumber && editorMatch) {
        this.openWithLineNumber(filePath, appName, editorMatch.config, options, projectRoot)
          .then(resolve)
          .catch(() => {
            // Fallback to regular open if line number open fails
            logger.warn('Line number open failed, falling back to regular open', { appName, filePath });
            this.openWithAppSimple(filePath, appName, projectRoot).then(resolve);
          });
        return;
      }

      // No line number or unsupported editor - use simple open
      // Only pass projectRoot for known editors; non-editor apps (Preview, Marked2, etc.)
      // don't understand --args and need LaunchServices file access via 'open -a'
      this.openWithAppSimple(filePath, appName, editorMatch ? projectRoot : null).then(resolve);
    });
  }

  /**
   * シンプルなアプリ起動（行番号なし）
   */
  private openWithAppSimple(filePath: string, appName: string, projectRoot?: string | null): Promise<OpenFileResult> {
    return new Promise((resolve) => {
      let openArgs: string[];
      if (projectRoot) {
        // Directories passed as second arg cause editors to open them as separate projects
        const isDir = this.isDirectory(filePath);
        openArgs = isDir
          ? ['-na', appName, '--args', projectRoot]
          : ['-na', appName, '--args', projectRoot, filePath];
      } else {
        openArgs = ['-a', appName, filePath];
      }

      execFile('open', openArgs, (error) => {
        if (error) {
          // アプリが見つからない等のエラー時はフォールバック
          logger.warn(`Failed to open with ${appName}, falling back to default`, {
            error: error.message,
            filePath
          });
          // フォールバック：システムデフォルトで開く
          this.openWithDefault(filePath).then(resolve);
          return;
        }

        logger.info('File opened successfully with configured app', {
          filePath,
          app: appName
        });
        resolve({ success: true });
      });
    });
  }

  /**
   * 行番号指定でファイルを開く
   */
  private openWithLineNumber(
    filePath: string,
    appName: string,
    config: EditorConfig,
    options: OpenFileOptions,
    projectRoot?: string | null
  ): Promise<OpenFileResult> {
    return new Promise((resolve, reject) => {
      // Comprehensive file path validation to prevent argument injection
      if (!filePath || typeof filePath !== 'string') {
        reject(new Error('Invalid file path: must be a non-empty string'));
        return;
      }
      if (filePath.startsWith('-') || filePath.includes('\0') || filePath.includes('\n') || filePath.includes('\r')) {
        reject(new Error('Invalid file path: contains forbidden characters'));
        return;
      }

      // Validate and clamp line/column numbers
      if (options.lineNumber !== undefined && (!Number.isInteger(options.lineNumber) || options.lineNumber < 1)) {
        reject(new Error('Invalid line number: must be a positive integer'));
        return;
      }
      if (options.columnNumber !== undefined && (!Number.isInteger(options.columnNumber) || options.columnNumber < 1)) {
        reject(new Error('Invalid column number: must be a positive integer'));
        return;
      }
      const lineNumber = Math.max(1, Math.min(options.lineNumber || 1, 999999));
      const columnNumber = Math.max(1, Math.min(options.columnNumber || 1, 9999));

      // Build app-specific arguments based on lineFormat
      const appArgs = this.buildLineArgs(filePath, lineNumber, columnNumber, config.lineFormat, projectRoot);

      if (config.cli) {
        // CLI-based launch (e.g. xed for Xcode)
        execFile(config.cli, appArgs, (error) => {
          if (error) {
            logger.warn('CLI open failed, falling back to open command', { error: error.message, cli: config.cli });
            // Fallback: open file without line number
            execFile('open', ['-a', appName, filePath], (fallbackError) => {
              if (fallbackError) {
                reject(fallbackError);
                return;
              }
              logger.info('File opened with fallback (no line jump)', { filePath, app: appName });
              resolve({ success: true });
            });
            return;
          }
          logger.info('File opened with line number', { filePath, lineNumber, cli: config.cli });
          resolve({ success: true });
        });
        return;
      }

      // Launch via macOS 'open -na <app> --args ...'
      const args = ['-na', appName, '--args', ...appArgs];
      execFile('open', args, (error) => {
        if (error) {
          logger.warn('open -na --args failed', { error: error.message, appName });
          reject(error);
          return;
        }
        logger.info('File opened with line number', { filePath, lineNumber, app: appName });
        resolve({ success: true });
      });
    });
  }

  /**
   * lineFormat に応じた引数配列を生成する
   */
  private buildLineArgs(filePath: string, lineNumber: number, columnNumber: number, lineFormat?: string, projectRoot?: string | null): string[] {
    switch (lineFormat) {
      case 'goto':
        // VSCode/Cursor/Windsurf style: --goto file:line:column
        return projectRoot
          ? [projectRoot, '--goto', `${filePath}:${lineNumber}:${columnNumber}`]
          : ['--goto', `${filePath}:${lineNumber}:${columnNumber}`];
      case 'colon':
        // Sublime/Zed style: file:line:column
        return projectRoot
          ? [projectRoot, `${filePath}:${lineNumber}:${columnNumber}`]
          : [`${filePath}:${lineNumber}:${columnNumber}`];
      case 'line':
        // TextMate/Xcode(xed) style: -l <line> file (no project root needed)
        return ['-l', String(lineNumber), filePath];
      default:
        // JetBrains IDEs: --line <line> [projectRoot] file
        return projectRoot
          ? ['--line', String(lineNumber), projectRoot, filePath]
          : ['--line', String(lineNumber), filePath];
    }
  }

  private isDirectory(filePath: string): boolean {
    try {
      return statSync(filePath).isDirectory();
    } catch {
      return false;
    }
  }

  /**
   * アプリ名がホワイトリストに登録されているか確認
   * EDITOR_CONFIGSに登録されているアプリのみを許可
   */
  private isWhitelistedApp(appName: string): boolean {
    return findEditorConfig(appName) !== null;
  }

  /**
   * macOSのデフォルトアプリを検出する
   * NSWorkspace APIを使用してファイルタイプに対応するデフォルトアプリ名を取得
   */
  private async detectDefaultApp(filePath: string): Promise<string | null> {
    if (process.platform !== 'darwin') return null;

    return new Promise((resolve) => {
      // Comprehensive path validation to prevent script injection
      if (!filePath || typeof filePath !== 'string') {
        logger.warn('Invalid file path type', { filePath });
        resolve(null);
        return;
      }

      // Reject paths with control characters, newlines, or other dangerous characters
      const dangerousChars = ['\n', '\r', '\0', '\t', '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\x0b', '\x0c', '\x0e', '\x0f'];
      if (dangerousChars.some(char => filePath.includes(char))) {
        logger.warn('Rejected file path with control characters', { filePath });
        resolve(null);
        return;
      }

      // Additional validation: reject paths with backticks or dollar signs that could be used in command substitution
      if (filePath.includes('`') || filePath.includes('$')) {
        logger.warn('Rejected file path with shell metacharacters', { filePath });
        resolve(null);
        return;
      }

      // Use JSON.stringify for safe JavaScript string escaping (handles quotes, backslashes, and special characters)
      const safePathJson = JSON.stringify(filePath);
      const script = `ObjC.import("AppKit");var ws=$.NSWorkspace.sharedWorkspace;var url=$.NSURL.fileURLWithPath(${safePathJson});var appUrl=ws.URLForApplicationToOpenURL(url);appUrl?ObjC.unwrap(appUrl.URLByDeletingPathExtension.lastPathComponent):""`;

      execFile('osascript', ['-l', 'JavaScript', '-e', script], { timeout: 3000 }, (error, stdout) => {
        if (error || !stdout?.trim()) {
          resolve(null);
          return;
        }

        const detectedApp = stdout.trim();

        // Validate detected app name against whitelist
        if (!this.isWhitelistedApp(detectedApp)) {
          logger.warn('Detected app not in whitelist', { detectedApp });
          resolve(null);
          return;
        }

        resolve(detectedApp);
      });
    });
  }

  /**
   * システムデフォルトで開く
   * openコマンドを使用してアプリをフォアグラウンドで開く
   */
  private openWithDefault(filePath: string): Promise<OpenFileResult> {
    return new Promise((resolve) => {
      // openコマンドを使用（shell.openPathはアプリをフロントに持ってこない場合がある）
      // -g オプションを付けないことでフォアグラウンドで開く
      execFile('open', [filePath], (error) => {
        if (error) {
          logger.error('Failed to open file with default app', {
            error: error.message,
            filePath
          });
          resolve({ success: false, error: error.message });
          return;
        }

        logger.info('File opened successfully with default app', { filePath });
        resolve({ success: true });
      });
    });
  }

  /**
   * 指定された拡張子に対して設定されているアプリ名を取得
   */
  getAppForExtension(extension: string): string | null {
    const settings = this.settingsManager.getSettings();
    const ext = extension.startsWith('.') ? extension.slice(1) : extension;
    return settings.fileOpener?.extensions?.[ext.toLowerCase()] || null;
  }

  /**
   * ファイルパスに対応するディレクトリ設定のエディタを取得
   * glob パターン（* / **）をサポート。最も具体的なパターンが優先される。
   */
  private findDirectoryEditor(filePath: string, directories?: FileOpenerDirectoryEntry[]): string | null {
    if (!directories || directories.length === 0) {
      return null;
    }

    const normalizedFilePath = path.resolve(filePath);
    let bestMatch: string | null = null;
    let bestSpecificity = -1;

    for (const entry of directories) {
      // ~ をホームディレクトリに展開
      const expandedDir = entry.path.startsWith('~')
        ? path.join(os.homedir(), entry.path.slice(1))
        : entry.path;
      const normalizedPattern = path.resolve(expandedDir);

      // パターンの具体性（globでない部分の長さ）を計算
      const specificity = this.getPatternSpecificity(normalizedPattern);

      // glob パターンを正規表現に変換してマッチ判定
      const regex = this.globToRegex(normalizedPattern);
      if (regex.test(normalizedFilePath) && specificity > bestSpecificity) {
        bestMatch = entry.editor;
        bestSpecificity = specificity;
      }
    }

    return bestMatch;
  }

  /**
   * glob パターンの具体性を返す（glob 以前の固定部分の長さ）
   * 具体性が高い（固定部分が長い）パターンが優先される
   */
  private getPatternSpecificity(pattern: string): number {
    const firstGlob = pattern.indexOf('*');
    return firstGlob === -1 ? pattern.length : firstGlob;
  }

  /**
   * ディレクトリ glob パターンを正規表現に変換
   * - ** → 任意のパス（/ を含む）
   * - * → 1階層分（/ を含まない）
   * パターン配下のファイルすべてにマッチする
   */
  private globToRegex(pattern: string): RegExp {
    // 特殊文字をエスケープ（* 以外）
    let regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
    // ** → 任意のパス
    regexStr = regexStr.replace(/\*\*/g, '§GLOBSTAR§');
    // * → 1階層
    regexStr = regexStr.replace(/\*/g, '[^/]*');
    regexStr = regexStr.replace(/§GLOBSTAR§/g, '.*');
    // パターン自体またはパターン配下のファイルにマッチ
    return new RegExp(`^${regexStr}(/|$)`);
  }

  /**
   * デフォルトエディタを取得
   */
  getDefaultEditor(): string | null {
    const settings = this.settingsManager.getSettings();
    return settings.fileOpener?.defaultEditor || null;
  }
}
