import 'dart:io';
import 'dart:math' as math;

import 'package:archive/archive.dart';
import 'package:path/path.dart' as p;
import 'package:xml/xml.dart';

import '../../diagnostics/badnote_log.dart';
import 'office_document.dart';

/// Native PPTX parser — no LibreOffice. Reads OOXML zip + slide XML.
class PptxParser {
  /// EMUs per English inch (Office drawing unit).
  static const double _emuPerInch = 914400;
  static const double _defaultDpi = 96;

  Future<ParsedPptx> parse(String pptxPath, {Directory? cacheDir, String? cacheDirPath}) async {
    BadNoteLog.instance.info(LogSubsystem.office, 'pptx_parse_start', fields: {
      'path': pptxPath,
    });
    final bytes = await File(pptxPath).readAsBytes();
    final archive = ZipDecoder().decodeBytes(bytes);

    Directory out = cacheDir ??
        (cacheDirPath != null
            ? Directory(cacheDirPath)
            : Directory(p.join(Directory.systemTemp.path, 'badnote_pptx_${DateTime.now().millisecondsSinceEpoch}')));
    if (!await out.exists()) await out.create(recursive: true);

    // Default slide size (widescreen 13.333" x 7.5") in pixels at 96dpi.
    double slideW = 13.333 * _defaultDpi;
    double slideH = 7.5 * _defaultDpi;
    final sldSz = _file(archive, 'ppt/presentation.xml');
    if (sldSz != null) {
      try {
        final doc = XmlDocument.parse(sldSz);
        final candidates = [
          ...doc.findAllElements('sldSz', namespace: '*'),
          ...doc.findAllElements('p:sldSz'),
        ];
        final el = candidates.isEmpty ? null : candidates.first;
        if (el != null) {
          final cx = int.tryParse(el.getAttribute('cx') ?? '') ?? 0;
          final cy = int.tryParse(el.getAttribute('cy') ?? '') ?? 0;
          if (cx > 0 && cy > 0) {
            slideW = cx / _emuPerInch * _defaultDpi;
            slideH = cy / _emuPerInch * _defaultDpi;
          }
        }
      } catch (_) {}
    }

    final slideFiles = archive.files
        .where((f) =>
            f.name.startsWith('ppt/slides/slide') &&
            f.name.endsWith('.xml') &&
            !f.name.contains('_rels'))
        .toList()
      ..sort((a, b) => _slideNum(a.name).compareTo(_slideNum(b.name)));

    final slides = <OfficeSlide>[];
    for (var i = 0; i < slideFiles.length; i++) {
      final file = slideFiles[i];
      final xml = _decode(file);
      if (xml == null) continue;
      final runs = <OfficeTextRun>[];
      final images = <OfficeImage>[];
      final textBuf = StringBuffer();

      try {
        final doc = XmlDocument.parse(xml);
        for (final t in doc.findAllElements('a:t')) {
          final text = t.innerText;
          if (text.isEmpty) continue;
          textBuf.writeln(text);
          // Approximate: stack text vertically when no transform is parsed.
          runs.add(OfficeTextRun(
            text: text,
            x: 48,
            y: 48.0 + runs.length * 28,
            width: math.max(120, slideW - 96),
            height: 28,
          ));
        }

        // Extract images referenced by this slide's relationships.
        final relsName =
            'ppt/slides/_rels/slide${_slideNum(file.name)}.xml.rels';
        final relsXml = _file(archive, relsName);
        if (relsXml != null) {
          final relsDoc = XmlDocument.parse(relsXml);
          for (final rel in relsDoc.findAllElements('Relationship')) {
            final type = rel.getAttribute('Type') ?? '';
            if (!type.contains('image')) continue;
            var target = rel.getAttribute('Target') ?? '';
            if (target.isEmpty) continue;
            // Targets are relative to ppt/slides/ → often ../media/image1.png
            final mediaPath = p.normalize(p.join('ppt/slides', target));
            final media = _archiveFile(archive, mediaPath) ??
                _archiveFile(archive, target.replaceFirst('../', 'ppt/'));
            if (media == null) continue;
            final content = media.content;
            final outPath = p.join(out.path, p.basename(mediaPath));
            await File(outPath).writeAsBytes(content);
            images.add(OfficeImage(
              bytesPath: outPath,
              x: 80,
              y: slideH * 0.35,
              width: slideW * 0.4,
              height: slideH * 0.4,
            ));
          }
        }
      } catch (e) {
        BadNoteLog.instance.warn(LogSubsystem.office, 'slide_parse_error', fields: {
          'slide': file.name,
          'error': '$e',
        });
      }

      slides.add(OfficeSlide(
        index: i,
        width: slideW,
        height: slideH,
        runs: runs,
        images: images,
        plainText: textBuf.toString().trim(),
      ));
    }

    BadNoteLog.instance.info(LogSubsystem.office, 'pptx_parse_done', fields: {
      'slides': slides.length,
    });
    return ParsedPptx(sourcePath: pptxPath, slides: slides);
  }

  Future<String> extractText(String pptxPath) async {
    final parsed = await parse(pptxPath);
    return parsed.allText;
  }

  static int _slideNum(String name) {
    final m = RegExp(r'slide(\d+)\.xml').firstMatch(name);
    return int.tryParse(m?.group(1) ?? '') ?? 0;
  }

  static String? _file(Archive archive, String name) {
    final f = _archiveFile(archive, name);
    return _decode(f);
  }

  static ArchiveFile? _archiveFile(Archive archive, String name) {
    final normalized = name.replaceAll('\\', '/');
    for (final f in archive.files) {
      if (f.name.replaceAll('\\', '/') == normalized) return f;
    }
    return null;
  }

  static String? _decode(ArchiveFile? file) {
    if (file == null) return null;
    return String.fromCharCodes(file.content);
  }
}
