/**
 * SFTP File Utilities
 * Helper functions for file type detection and extension handling
 */

import { netcattyBridge } from "../infrastructure/services/netcattyBridge";

// Common text file extensions
const TEXT_EXTENSIONS = new Set([
  // Code/Scripts
  'js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'vue', 'svelte',
  'py', 'pyw', 'pyi',
  'sh', 'bash', 'zsh', 'fish', 'bat', 'cmd', 'ps1', 'psm1',
  'c', 'cpp', 'h', 'hpp', 'cc', 'cxx', 'hh', 'hxx',
  'java', 'scala', 'kt', 'kts', 'groovy', 'gradle',
  'go', 'rs', 'rb', 'php', 'pl', 'pm', 'lua', 'r', 'R',
  'swift', 'dart', 'cs', 'fs', 'vb',
  'ex', 'exs', 'erl', 'hrl', 'clj', 'cljs', 'cljc',
  'hs', 'lhs', 'elm', 'ml', 'mli', 'nim',
  // Web
  'html', 'htm', 'xhtml', 'css', 'scss', 'sass', 'less', 'styl',
  // Config/Data
  'json', 'json5', 'jsonc', 'xml', 'xsl', 'xslt', 'xsd',
  'yml', 'yaml', 'toml', 'ini', 'conf', 'cfg', 'config', 'properties',
  'env', 'gitignore', 'gitattributes', 'editorconfig', 'eslintrc', 'prettierrc',
  'sql', 'graphql', 'gql',
  // Text/Docs
  'md', 'markdown', 'mdx', 'txt', 'text', 'log', 'rst', 'adoc', 'asciidoc',
  'tex', 'latex', 'bib',
  // Data formats
  'csv', 'tsv', 'psv',
  // System
  'rc', 'bashrc', 'zshrc', 'profile', 'vimrc', 'tmux', 'nanorc',
  'dockerfile', 'containerfile', 'makefile', 'cmake', 'mak',
  // Version control & Git
  'gitconfig', 'gitmodules', 'gitkeep',
  // Other common text formats
  'diff', 'patch', 'htaccess', 'lock', 'sum',
  // Service/System files
  'service', 'socket', 'timer', 'mount', 'automount', 'target',
  // Shell history and data
  'history', 'zsh_history', 'bash_history',
]);

// Additional filenames (no extension) that are always text
const TEXT_FILENAMES = new Set([
  'readme', 'license', 'licence', 'changelog', 'authors', 'contributors',
  'copying', 'install', 'news', 'todo', 'history', 'makefile', 'dockerfile',
  'gemfile', 'rakefile', 'brewfile', 'procfile', 'vagrantfile',
  'cmakelists.txt', 'cmakelists',
]);

// Common image file extensions
const IMAGE_EXTENSIONS = new Set([
  'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg',
  'ico', 'tiff', 'tif', 'heic', 'heif', 'avif', 'jfif',
]);

// Known binary file extensions - files that should never be opened as text
const BINARY_EXTENSIONS = new Set([
  // Images
  'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'ico', 'tiff', 'tif',
  'heic', 'heif', 'avif', 'jfif', 'psd', 'ai', 'eps', 'raw', 'cr2', 'nef',
  // Audio
  'mp3', 'wav', 'flac', 'aac', 'ogg', 'wma', 'm4a', 'aiff', 'opus',
  // Video
  'mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'webm', 'm4v', '3gp', 'mpeg', 'mpg',
  // Archives
  'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'lz', 'lzma', 'zst',
  'tgz', 'tbz2', 'txz', 'cab', 'iso', 'dmg',
  // Executables
  'exe', 'dll', 'so', 'dylib', 'bin', 'app', 'msi', 'deb', 'rpm',
  'apk', 'ipa', 'jar', 'war', 'ear',
  // Documents (binary formats)
  'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp',
  // Fonts
  'ttf', 'otf', 'woff', 'woff2', 'eot',
  // Database
  'db', 'sqlite', 'sqlite3', 'mdb', 'accdb',
  // Object files
  'o', 'obj', 'pyc', 'pyo', 'class', 'beam',
  // Other binary
  'swf', 'fla', 'blend', 'unity3d', 'unitypackage',
]);

// MIME types for images (for creating blob URLs)
const IMAGE_MIME_TYPES: Record<string, string> = {
  jpg: 'image/jpeg',
  jpeg: 'image/jpeg',
  jfif: 'image/jpeg',
  png: 'image/png',
  gif: 'image/gif',
  bmp: 'image/bmp',
  webp: 'image/webp',
  svg: 'image/svg+xml',
  ico: 'image/x-icon',
  tiff: 'image/tiff',
  tif: 'image/tiff',
  heic: 'image/heic',
  heif: 'image/heif',
  avif: 'image/avif',
};

// Language IDs for syntax highlighting
const EXTENSION_TO_LANGUAGE: Record<string, string> = {
  js: 'javascript',
  jsx: 'javascript',
  mjs: 'javascript',
  cjs: 'javascript',
  ts: 'typescript',
  tsx: 'typescript',
  py: 'python',
  pyw: 'python',
  pyi: 'python',
  sh: 'shell',
  bash: 'shell',
  zsh: 'shell',
  fish: 'shell',
  bat: 'batch',
  cmd: 'batch',
  ps1: 'powershell',
  psm1: 'powershell',
  c: 'c',
  cpp: 'cpp',
  h: 'c',
  hpp: 'cpp',
  cc: 'cpp',
  cxx: 'cpp',
  java: 'java',
  kt: 'kotlin',
  kts: 'kotlin',
  go: 'go',
  rs: 'rust',
  rb: 'ruby',
  php: 'php',
  pl: 'perl',
  lua: 'lua',
  r: 'r',
  R: 'r',
  swift: 'swift',
  dart: 'dart',
  cs: 'csharp',
  fs: 'fsharp',
  vb: 'vb',
  html: 'html',
  htm: 'html',
  xhtml: 'html',
  css: 'css',
  scss: 'scss',
  sass: 'sass',
  less: 'less',
  json: 'json',
  jsonc: 'jsonc',
  json5: 'json5',
  xml: 'xml',
  xsl: 'xml',
  xslt: 'xml',
  yml: 'yaml',
  yaml: 'yaml',
  toml: 'toml',
  ini: 'ini',
  conf: 'ini',
  cfg: 'ini',
  sql: 'sql',
  graphql: 'graphql',
  gql: 'graphql',
  md: 'markdown',
  markdown: 'markdown',
  mdx: 'markdown',
  txt: 'plaintext',
  log: 'plaintext',
  vue: 'vue',
  svelte: 'svelte',
  dockerfile: 'dockerfile',
  makefile: 'makefile',
  diff: 'diff',
  patch: 'diff',
};

/**
 * Get the file extension from a filename
 * For files without extension, returns 'file'
 */
export function getFileExtension(fileName: string): string {
  const lastDot = fileName.lastIndexOf('.');
  if (lastDot === -1 || lastDot === 0) {
    return 'file'; // No extension or hidden file without extension
  }
  return fileName.slice(lastDot + 1).toLowerCase();
}

/** True when the filename has a real extension (e.g. `foo.txt`), not a dot-only name like `.git`. */
export function hasFileExtension(fileName: string): boolean {
  return fileName.lastIndexOf('.') > 0;
}

/**
 * Check if a file is a text file based on its extension and name
 */
export function isTextFile(fileName: string): boolean {
  const ext = getFileExtension(fileName);

  // Check known text extensions
  if (TEXT_EXTENSIONS.has(ext)) {
    return true;
  }

  // Check common filenames that are text but have no extension
  const baseName = fileName.toLowerCase().split('/').pop() || '';
  const nameWithoutExt = baseName.replace(/\.[^.]+$/, '');

  // Check exact filename matches
  if (TEXT_FILENAMES.has(baseName) || TEXT_FILENAMES.has(nameWithoutExt)) {
    return true;
  }

  // Check dot-files that are typically text config files
  if (baseName.startsWith('.')) {
    const dotConfigPatterns = [
      /^\.(git|npm|yarn|docker|eslint|prettier|babel|env)/,
      /^\.(nvmrc|ruby-version|python-version|node-version)$/,
      /rc$/, // Files ending with 'rc' like .bashrc, .vimrc
    ];
    if (dotConfigPatterns.some(pattern => pattern.test(baseName))) {
      return true;
    }
  }

  return false;
}

/**
 * Check if binary data appears to be text by analyzing byte patterns
 * This provides a more accurate detection than extension-only checking
 * 
 * @param data - First chunk of file data (ArrayBuffer or Uint8Array)
 * @param maxBytes - Maximum bytes to check (default 512)
 * @returns true if data appears to be text
 */
export function isTextData(data: ArrayBuffer | Uint8Array, maxBytes: number = 512): boolean {
  const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
  const checkLength = Math.min(bytes.length, maxBytes);

  if (checkLength === 0) return true; // Empty file is considered text

  let controlChars = 0;
  let nullBytes = 0;
  let highBytes = 0;
  let totalBytes = 0;

  for (let i = 0; i < checkLength; i++) {
    const byte = bytes[i];
    totalBytes++;

    // Null bytes are strong indicators of binary files
    if (byte === 0) {
      nullBytes++;
      if (nullBytes > 0) return false; // Even one null byte suggests binary
    }

    // Control characters (except common ones like \t, \n, \r)
    if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) {
      controlChars++;
    }

    // High-bit characters (non-ASCII) - some are OK for UTF-8
    if (byte > 127) {
      highBytes++;
    }
  }

  // If more than 30% are control chars or more than 95% are high-bit chars, likely binary
  const controlRatio = controlChars / totalBytes;
  const highRatio = highBytes / totalBytes;

  if (controlRatio > 0.3) return false;
  if (highRatio > 0.95) return false;

  return true;
}

/**
 * Enhanced text file detection combining extension and content analysis
 * Use this when you have access to file data for better accuracy
 */
export function isTextFileEnhanced(fileName: string, data?: ArrayBuffer | Uint8Array): boolean {
  // First check by extension
  const extCheck = isTextFile(fileName);

  // If we have data, verify it's actually text
  if (data && data.byteLength > 0) {
    return extCheck && isTextData(data);
  }

  // Fall back to extension-only check
  return extCheck;
}

/**
 * Check if a file is definitely a binary file based on its extension.
 * Used to exclude files from "Edit" option in context menu.
 */
export function isKnownBinaryFile(fileName: string): boolean {
  const ext = getFileExtension(fileName);
  return BINARY_EXTENSIONS.has(ext);
}

/**
 * Check if a file could potentially be opened as text.
 * This is more permissive than isTextFile - it returns true for any file
 * that is not a known binary file. Used for showing "Edit" in context menu.
 * Actual text detection should be done by reading file content.
 */
export function couldBeTextFile(fileName: string): boolean {
  // If it's a known binary file, definitely not text
  if (isKnownBinaryFile(fileName)) {
    return false;
  }
  // Otherwise, it could be text - we'll verify when actually opening
  return true;
}

/**
 * Check if a file is an image file based on its extension
 */
export function isImageFile(fileName: string): boolean {
  const ext = getFileExtension(fileName);
  return IMAGE_EXTENSIONS.has(ext);
}

/**
 * Get MIME type for an image file
 */
export function getImageMimeType(fileName: string): string {
  const ext = getFileExtension(fileName);
  return IMAGE_MIME_TYPES[ext] || 'application/octet-stream';
}

/**
 * Get language ID for syntax highlighting
 */
export function getLanguageId(fileName: string): string {
  const ext = getFileExtension(fileName);
  return EXTENSION_TO_LANGUAGE[ext] || 'plaintext';
}

/**
 * Get a user-friendly name for a language
 */
export function getLanguageName(languageId: string): string {
  const names: Record<string, string> = {
    javascript: 'JavaScript',
    typescript: 'TypeScript',
    python: 'Python',
    shell: 'Shell',
    batch: 'Batch',
    powershell: 'PowerShell',
    c: 'C',
    cpp: 'C++',
    java: 'Java',
    kotlin: 'Kotlin',
    go: 'Go',
    rust: 'Rust',
    ruby: 'Ruby',
    php: 'PHP',
    perl: 'Perl',
    lua: 'Lua',
    r: 'R',
    swift: 'Swift',
    dart: 'Dart',
    csharp: 'C#',
    fsharp: 'F#',
    vb: 'Visual Basic',
    html: 'HTML',
    css: 'CSS',
    scss: 'SCSS',
    sass: 'Sass',
    less: 'Less',
    json: 'JSON',
    jsonc: 'JSON with Comments',
    json5: 'JSON5',
    xml: 'XML',
    yaml: 'YAML',
    toml: 'TOML',
    ini: 'INI',
    sql: 'SQL',
    graphql: 'GraphQL',
    markdown: 'Markdown',
    plaintext: 'Plain Text',
    vue: 'Vue',
    svelte: 'Svelte',
    dockerfile: 'Dockerfile',
    makefile: 'Makefile',
    diff: 'Diff',
  };
  return names[languageId] || languageId.charAt(0).toUpperCase() + languageId.slice(1);
}

/**
 * File opener application types
 * - 'builtin-editor': Built-in text editor (Monaco)
 * - 'system-app': External system application (stores path)
 */
export type FileOpenerType = 'builtin-editor' | 'system-app';

/**
 * System application info for file associations
 */
export interface SystemAppInfo {
  path: string;  // Path to the executable/app
  name: string;  // Display name
}

/**
 * File association record
 */
export interface FileAssociation {
  extension: string;
  openerType: FileOpenerType;
  systemApp?: SystemAppInfo;  // Only set when openerType is 'system-app'
}

/**
 * Get all supported language IDs for syntax highlighting dropdown
 */
export function getSupportedLanguages(): { id: string; name: string }[] {
  const languageIds = new Set(Object.values(EXTENSION_TO_LANGUAGE));
  languageIds.add('plaintext');

  return Array.from(languageIds)
    .map(id => ({ id, name: getLanguageName(id) }))
    .sort((a, b) => a.name.localeCompare(b.name));
}

/**
 * Represents a file or directory entry from drag-and-drop
 * This includes the relative path for nested files in folders
 */
export interface DropEntry {
  file: File | null;  // null for directory entries
  localPath?: string;
  size?: number;
  relativePath: string;  // Path relative to the root of the drop (e.g., "folder/subfolder/file.txt")
  isDirectory: boolean;
}

const createDropEntriesFromFiles = (files: FileList | File[]): DropEntry[] => {
  const results: DropEntry[] = [];
  for (let i = 0; i < files.length; i++) {
    const file = files[i];
    const path = getPathForFile(file);
    if (path) {
      (file as File & { path?: string }).path = path;
    }
    results.push({
      file,
      relativePath: (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name,
      isDirectory: false,
    });
  }
  return results;
};

/**
 * Convert a FileSystemEntry to a File
 */
function entryToFile(entry: FileSystemFileEntry): Promise<File> {
  return new Promise((resolve, reject) => {
    entry.file(resolve, reject);
  });
}

/**
 * Read all entries from a directory reader
 * Handles the fact that readEntries may not return all entries at once
 */
async function readAllDirectoryEntries(
  directoryReader: FileSystemDirectoryReader
): Promise<FileSystemEntry[]> {
  const allEntries: FileSystemEntry[] = [];

  // Keep reading until we get an empty result
  let entries: FileSystemEntry[];
  do {
    entries = await new Promise<FileSystemEntry[]>((resolve, reject) => {
      directoryReader.readEntries(resolve, reject);
    });
    for (const entry of entries) {
      allEntries.push(entry);
    }
  } while (entries.length > 0);

  return allEntries;
}

/**
 * Process file system entries iteratively (non-recursive) to handle large folders
 * Uses a queue-based approach to avoid stack overflow
 * @param rootEntries - The root entries to process
 * @returns Array of DropEntry objects with files and their relative paths
 */
async function processEntriesIteratively(
  rootEntries: FileSystemEntry[]
): Promise<DropEntry[]> {
  const results: DropEntry[] = [];

  // Queue of entries to process: [entry, basePath]
  const queue: Array<{ entry: FileSystemEntry; basePath: string }> = [];

  // Initialize queue with root entries
  for (const entry of rootEntries) {
    queue.push({ entry, basePath: "" });
  }

  let processedCount = 0;
  const YIELD_INTERVAL = 100; // Yield to main thread every N items

  while (queue.length > 0) {
    const { entry, basePath } = queue.shift()!;

    if (entry.isFile) {
      const fileEntry = entry as FileSystemFileEntry;
      try {
        const file = await entryToFile(fileEntry);
        results.push({
          file,
          relativePath: basePath ? `${basePath}/${entry.name}` : entry.name,
          isDirectory: false,
        });
      } catch (error) {
        console.warn(`Failed to read file entry: ${entry.name}`, error);
      }
    } else if (entry.isDirectory) {
      const dirEntry = entry as FileSystemDirectoryEntry;
      const currentPath = basePath ? `${basePath}/${entry.name}` : entry.name;

      // Add directory entry
      results.push({
        file: null,
        relativePath: currentPath,
        isDirectory: true,
      });

      try {
        const reader = dirEntry.createReader();
        const childEntries = await readAllDirectoryEntries(reader);

        // Add child entries to the queue (not recursive!)
        for (const childEntry of childEntries) {
          queue.push({ entry: childEntry, basePath: currentPath });
        }
      } catch (error) {
        console.warn(`Failed to read directory: ${entry.name}`, error);
      }
    }

    // Yield to main thread periodically to keep UI responsive
    processedCount++;
    if (processedCount % YIELD_INTERVAL === 0) {
      await new Promise<void>(resolve => setTimeout(resolve, 0));
    }
  }

  return results;
}

/**
 * Get the local file path for a File object using Electron's webUtils API
 * Falls back to the legacy file.path property if webUtils is not available
 */
export function getPathForFile(file: File): string | undefined {
  try {
    // Try Electron's webUtils API (exposed via preload)
    const path = netcattyBridge.get()?.getPathForFile?.(file);
    if (path) return path;
    // Fallback: try legacy file.path property
    return (file as File & { path?: string }).path;
  } catch {
    return undefined;
  }
}

/**
 * Extract all files and directories from a DataTransfer object
 * Supports both regular files and folders dropped from the OS
 *
 * Uses the webkitGetAsEntry() API for folder access, with fallback
 * to regular FileList for browsers that don't support it.
 *
 * @param dataTransfer - The DataTransfer object from a drop event
 * @returns Array of DropEntry objects with files and relative paths
 */
export async function extractDropEntries(
  dataTransfer: DataTransfer
): Promise<DropEntry[]> {
  const items = dataTransfer.items;

  // Build a map of file/folder name to path from the original files in DataTransfer.files
  const filePathMap = new Map<string, string>();
  const filesWithPath = dataTransfer.files;
  for (let i = 0; i < filesWithPath.length; i++) {
    const f = filesWithPath[i];
    const path = getPathForFile(f);
    if (path) {
      filePathMap.set(f.name, path);
    }
  }

  // Check if webkitGetAsEntry is supported (for folder access)
  if (items && items.length > 0 && typeof items[0].webkitGetAsEntry === 'function') {
    // Collect all entries first (getAsEntry must be called synchronously)
    const entries: FileSystemEntry[] = [];
    for (let i = 0; i < items.length; i++) {
      const item = items[i];
      if (item.kind === 'file') {
        const entry = item.webkitGetAsEntry();
        if (entry) {
          entries.push(entry);
        }
      }
    }

    // Process entries iteratively (non-recursive) to avoid stack overflow
    const results = await processEntriesIteratively(entries);
    if (results.length === 0) {
      return createDropEntriesFromFiles(dataTransfer.files);
    }

    // Restore the 'path' property for all files
    // Try to get the path directly from webUtils.getPathForFile for each file
    // This is more reliable than trying to reconstruct from folder paths
    for (const result of results) {
      if (result.file) {
        // First try to get path directly from the file
        const directPath = getPathForFile(result.file);
        if (directPath) {
          (result.file as File & { path?: string }).path = directPath;
        } else {
          // Fallback: try to reconstruct from root folder path
          const pathParts = result.relativePath.split('/');
          const rootName = pathParts[0];
          const rootPath = filePathMap.get(rootName);

          if (rootPath) {
            if (pathParts.length === 1) {
              // Root-level file: use the path directly
              (result.file as File & { path?: string }).path = rootPath;
            } else {
              // Nested file in a folder: construct full path
              // rootPath is the path to the root folder, we need to append the rest
              const restOfPath = pathParts.slice(1).join('/');
              const separator = rootPath.includes('\\') ? '\\' : '/';
              const fullPath = rootPath + separator + restOfPath.replace(/\//g, separator);
              (result.file as File & { path?: string }).path = fullPath;
            }
          }
        }
      }
    }

    return results;
  } else {
    // Fallback: use regular FileList (no folder support)
    // Files from FileList in Electron already have the 'path' property
    return createDropEntriesFromFiles(dataTransfer.files);
  }
}
