import 'dart:io';

import 'package:file_picker/file_picker.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';

import 'office/office_document.dart';
import 'office/pptx_parser.dart';

/// Service for processing PPTX files: text extraction, structured slide parse,
/// optional LibreOffice image conversion, and file picking.
///
/// **Native OOXML parsing is primary** ([PptxParser] via `package:archive` +
/// `package:xml`). LibreOffice (`soffice`) is an optional fallback ONLY when
/// the native path fails AND the binary is present on the machine.
class PptxService {
  static const _uuid = Uuid();

  final PptxParser _parser;

  PptxService({PptxParser? parser}) : _parser = parser ?? PptxParser();

  /// Extract all text content from a PPTX file.
  ///
  /// Prefers the native [PptxParser]. Falls back to a legacy unzip+regex path
  /// only if native parsing throws.
  Future<String> extractText(String pptxPath) async {
    try {
      final parsed = await _parser.parse(pptxPath);
      return parsed.plainText;
    } catch (_) {
      return _extractTextLegacy(pptxPath);
    }
  }

  /// Parse PPTX into structured slides (text runs with approximate positions,
  /// embedded images extracted to a cache dir). Native-only — no LibreOffice.
  Future<ParsedPptx> parseSlides(String pptxPath, {String? cacheDir}) {
    return _parser.parse(pptxPath, cacheDirPath: cacheDir);
  }

  /// Convert PPTX slides to a list of image file paths (legacy PenSlideScreen).
  ///
  /// Prefer [parseSlides] for native text+image rendering. This method only
  /// invokes LibreOffice when native parse fails AND `soffice` exists;
  /// otherwise it emits placeholder PNGs from the native slide count.
  Future<List<String>> convertToImages(String pptxPath) async {
    try {
      final parsed = await _parser.parse(pptxPath);
      // Native succeeded — do NOT call LibreOffice; placeholders for callers
      // that still expect image paths. OfficeDocumentScreen uses [parseSlides].
      if (parsed.slides.isEmpty) return [];
      return _generatePlaceholderImages(pptxPath);
    } catch (_) {
      // Native failed — LibreOffice fallback ONLY if soffice exists.
      final soffice = await resolveSoffice();
      if (soffice != null) {
        final loImages = await _convertViaLibreOffice(pptxPath);
        if (loImages.isNotEmpty) return loImages;
      }
      return _generatePlaceholderImages(pptxPath);
    }
  }

  /// Open a file picker dialog and return the selected PPTX path, or null.
  Future<String?> openPptxFile() async {
    final result = await FilePicker.platform.pickFiles(
      type: FileType.custom,
      allowedExtensions: ['pptx', 'ppt'],
    );
    final files = result?.files;
    if (files == null || files.isEmpty) return null;
    return files.first.path;
  }

  // ---------------------------------------------------------------------------
  // Legacy / LibreOffice helpers
  // ---------------------------------------------------------------------------

  Future<String> _extractTextLegacy(String pptxPath) async {
    final tmpDir = await _makeTmpDir('pptx_text');

    try {
      final unzipResult = await Process.run('unzip', [
        '-o',
        '-q',
        pptxPath,
        '-d',
        tmpDir.path,
      ]);

      if (unzipResult.exitCode != 0) {
        return '';
      }

      final slidesDir = Directory(p.join(tmpDir.path, 'ppt', 'slides'));
      if (!await slidesDir.exists()) return '';

      final slideFiles = await slidesDir
          .list()
          .where((f) => f.path.contains(RegExp(r'slide\d+\.xml$')))
          .toList();

      slideFiles.sort((a, b) {
        final aNum = _extractSlideNumber(a.path);
        final bNum = _extractSlideNumber(b.path);
        return aNum.compareTo(bNum);
      });

      final buffer = StringBuffer();
      for (final slideFile in slideFiles) {
        final xml = await File(slideFile.path).readAsString();
        final slideText = _extractTextFromXml(xml);
        if (slideText.isNotEmpty) {
          final num = _extractSlideNumber(slideFile.path);
          buffer.writeln('--- Slide $num ---');
          buffer.writeln(slideText);
          buffer.writeln();
        }
      }

      return buffer.toString().trim();
    } catch (_) {
      return '';
    } finally {
      try {
        await tmpDir.delete(recursive: true);
      } catch (_) {}
    }
  }

  String _extractTextFromXml(String xml) {
    final lines = <String>[];
    final regex = RegExp(r'<a:t[^>]*>(.*?)</a:t>', dotAll: true);
    for (final match in regex.allMatches(xml)) {
      final text = match.group(1) ?? '';
      if (text.trim().isNotEmpty) {
        lines.add(text.trim());
      }
    }
    return lines.join('\n');
  }

  int _extractSlideNumber(String path) {
    final match = RegExp(r'slide(\d+)\.xml$').firstMatch(path);
    if (match != null) return int.parse(match.group(1)!);
    return 0;
  }

  /// LibreOffice fallback — only when native fails or caller wants PNGs and
  /// soffice is installed.
  Future<List<String>> _convertViaLibreOffice(String pptxPath) async {
    try {
      final soffice = await resolveSoffice();
      if (soffice == null) return [];

      final outDir = await _makeTmpDir('pptx_images');

      final result = await Process.run(soffice, [
        '--headless',
        '--convert-to',
        'png',
        '--outdir',
        outDir.path,
        pptxPath,
      ]);

      if (result.exitCode != 0) return [];

      final pngs = await outDir
          .list()
          .where((f) => f.path.endsWith('.png'))
          .map((f) => f.path)
          .toList();

      pngs.sort();

      final persistDir = await _makeTmpDir('pptx_slides');
      final persistentPaths = <String>[];
      for (var i = 0; i < pngs.length; i++) {
        final src = File(pngs[i]);
        final dst = p.join(persistDir.path, 'slide_${i + 1}.png');
        await src.copy(dst);
        persistentPaths.add(dst);
      }

      try {
        await outDir.delete(recursive: true);
      } catch (_) {}

      return persistentPaths;
    } catch (_) {
      return [];
    }
  }

  /// Resolve the LibreOffice CLI binary, or null when unavailable.
  static Future<String?> resolveSoffice() async {
    if (Platform.isWindows) {
      const candidates = [
        r'C:\Program Files\LibreOffice\program\soffice.exe',
        r'C:\Program Files (x86)\LibreOffice\program\soffice.exe',
      ];
      for (final c in candidates) {
        if (await File(c).exists()) return c;
      }
      if (await _whichOk('soffice')) return 'soffice';
      return null;
    }
    if (await _whichOk('libreoffice')) return 'libreoffice';
    if (await _whichOk('soffice')) return 'soffice';
    return null;
  }

  static Future<bool> _whichOk(String cmd) async {
    try {
      final r = await Process.run('which', [cmd]);
      return r.exitCode == 0;
    } catch (_) {
      return false;
    }
  }

  /// Convert an arbitrary office document (e.g. DOCX) to PDF via LibreOffice.
  /// Optional — native [DocxParser] is preferred for opening in BadNote.
  Future<String?> convertToPdf(String sourcePath) async {
    final soffice = await resolveSoffice();
    if (soffice == null) return null;

    final outDir = p.dirname(sourcePath);
    try {
      final result = await Process.run(soffice, [
        '--headless',
        '--convert-to',
        'pdf',
        '--outdir',
        outDir,
        sourcePath,
      ]);
      if (result.exitCode != 0) return null;
      final pdfPath = p.join(
        outDir,
        '${p.basenameWithoutExtension(sourcePath)}.pdf',
      );
      if (await File(pdfPath).exists()) return pdfPath;
      return null;
    } catch (_) {
      return null;
    }
  }

  Future<List<String>> _generatePlaceholderImages(String pptxPath) async {
    final slideCount = await _countSlidesNative(pptxPath);
    if (slideCount == 0) return [];

    final outDir = await _makeTmpDir('pptx_placeholders');
    final paths = <String>[];

    final hasConvert = await _hasCommand('convert');

    for (var i = 1; i <= slideCount; i++) {
      final path = p.join(outDir.path, 'slide_$i.png');
      if (hasConvert) {
        await _generateWithImageMagick(path, i, slideCount);
      } else {
        await _writeMinimalPng(path);
      }
      paths.add(path);
    }

    return paths;
  }

  Future<int> _countSlidesNative(String pptxPath) async {
    try {
      final parsed = await _parser.parse(pptxPath);
      return parsed.slides.length;
    } catch (_) {
      return _countSlidesUnzip(pptxPath);
    }
  }

  Future<int> _countSlidesUnzip(String pptxPath) async {
    final tmpDir = await _makeTmpDir('pptx_count');
    try {
      await Process.run('unzip', ['-o', '-q', pptxPath, '-d', tmpDir.path]);
      final slidesDir = Directory(p.join(tmpDir.path, 'ppt', 'slides'));
      if (!await slidesDir.exists()) return 0;
      final count = await slidesDir
          .list()
          .where((f) => f.path.contains(RegExp(r'slide\d+\.xml$')))
          .length;
      return count;
    } catch (_) {
      return 0;
    } finally {
      try {
        await tmpDir.delete(recursive: true);
      } catch (_) {}
    }
  }

  Future<bool> _hasCommand(String cmd) async {
    try {
      final result = await Process.run('which', [cmd]);
      return result.exitCode == 0;
    } catch (_) {
      return false;
    }
  }

  Future<void> _generateWithImageMagick(
    String outPath,
    int slideNum,
    int total,
  ) async {
    final hue = ((slideNum - 1) * 137) % 360;
    await Process.run('convert', [
      '-size',
      '1920x1080',
      'xc:hsl($hue, 60%, 92%)',
      '-gravity',
      'center',
      '-pointsize',
      '120',
      '-fill',
      'hsl($hue, 30%, 40%)',
      '-annotate',
      '+0+0',
      'Slide $slideNum / $total',
      outPath,
    ]);
  }

  Future<void> _writeMinimalPng(String path) async {
    const pngBytes = <int>[
      0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
      0x00, 0x00, 0x00, 0x0D,
      0x49, 0x48, 0x44, 0x52,
      0x00, 0x00, 0x00, 0x01,
      0x00, 0x00, 0x00, 0x01,
      0x08, 0x02,
      0x00, 0x00, 0x00,
      0x90, 0x77, 0x53, 0xDE,
      0x00, 0x00, 0x00, 0x0C,
      0x49, 0x44, 0x41, 0x54,
      0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00,
      0x01, 0x01, 0x01, 0x00,
      0x18, 0xDD, 0x8D, 0xB4,
      0x00, 0x00, 0x00, 0x00,
      0x49, 0x45, 0x4E, 0x44,
      0xAE, 0x42, 0x60, 0x82,
    ];
    await File(path).writeAsBytes(pngBytes);
  }

  Future<Directory> _makeTmpDir(String prefix) async {
    final base = await getTemporaryDirectory();
    final dir = Directory(p.join(base.path, '${prefix}_${_uuid.v4()}'));
    await dir.create(recursive: true);
    return dir;
  }
}
