import 'dart:io';

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 DOCX parser — block-level structure for BadNote annotation pages.
class DocxParser {
  Future<ParsedDocx> parse(String docxPath, {Directory? cacheDir}) async {
    BadNoteLog.instance.info(LogSubsystem.office, 'docx_parse_start', fields: {
      'path': docxPath,
    });
    final bytes = await File(docxPath).readAsBytes();
    final archive = ZipDecoder().decodeBytes(bytes);
    Directory out = cacheDir ??
        Directory(p.join(Directory.systemTemp.path, 'badnote_docx_${DateTime.now().millisecondsSinceEpoch}'));
    if (!await out.exists()) await out.create(recursive: true);

    final documentXml = _decode(_find(archive, 'word/document.xml'));
    if (documentXml == null) {
      return ParsedDocx(sourcePath: docxPath, blocks: const []);
    }

    // Media map from relationships.
    final media = <String, String>{};
    final rels = _decode(_find(archive, 'word/_rels/document.xml.rels'));
    if (rels != null) {
      try {
        final relDoc = XmlDocument.parse(rels);
        for (final rel in relDoc.findAllElements('Relationship')) {
          final id = rel.getAttribute('Id');
          final type = rel.getAttribute('Type') ?? '';
          final target = rel.getAttribute('Target') ?? '';
          if (id == null || !type.contains('image') || target.isEmpty) continue;
          final mediaPath = p.normalize(p.join('word', target));
          final file = _find(archive, mediaPath);
          if (file?.content is! List<int>) continue;
          final outPath = p.join(out.path, p.basename(mediaPath));
          await File(outPath).writeAsBytes(file!.content as List<int>);
          media[id] = outPath;
        }
      } catch (_) {}
    }

    final blocks = <DocBlock>[];
    try {
      final doc = XmlDocument.parse(documentXml);
      for (final pEl in doc.findAllElements('w:p')) {
        final style = pEl
            .findElements('w:pPr')
            .expand((e) => e.findElements('w:pStyle'))
            .map((e) => e.getAttribute('w:val') ?? '')
            .firstWhere((s) => s.isNotEmpty, orElse: () => '');
        final texts = pEl.findAllElements('w:t').map((t) => t.innerText).join();
        final blips = pEl.findAllElements('a:blip');
        for (final blip in blips) {
          final embed = blip.getAttribute('r:embed') ?? blip.getAttribute('embed');
          if (embed != null && media[embed] != null) {
            blocks.add(DocBlock(
              type: DocBlockType.image,
              text: '',
              imagePath: media[embed],
            ));
          }
        }
        if (texts.trim().isEmpty && blips.isEmpty) continue;
        if (texts.trim().isEmpty) continue;
        final isHeading = style.toLowerCase().startsWith('heading') ||
            RegExp(r'^Heading\s*\d', caseSensitive: false).hasMatch(style);
        final level = int.tryParse(RegExp(r'(\d+)').firstMatch(style)?.group(1) ?? '') ??
            (isHeading ? 1 : 0);
        blocks.add(DocBlock(
          type: isHeading ? DocBlockType.heading : DocBlockType.paragraph,
          text: texts,
          level: level,
        ));
      }
      // Tables
      for (final row in doc.findAllElements('w:tr')) {
        final cells = row
            .findElements('w:tc')
            .map((tc) => tc.findAllElements('w:t').map((t) => t.innerText).join())
            .where((s) => s.trim().isNotEmpty)
            .join(' | ');
        if (cells.isEmpty) continue;
        blocks.add(DocBlock(type: DocBlockType.tableRow, text: cells));
      }
    } catch (e) {
      BadNoteLog.instance.warn(LogSubsystem.office, 'docx_parse_error', fields: {
        'error': '$e',
      });
    }

    BadNoteLog.instance.info(LogSubsystem.office, 'docx_parse_done', fields: {
      'blocks': blocks.length,
    });
    return ParsedDocx(sourcePath: docxPath, blocks: blocks);
  }

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

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

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