import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;

import '../../diagnostics/badnote_log.dart';
import '../../diagnostics/pen_event_ring.dart';
import '../../services/office/docx_parser.dart';
import '../../services/office/office_document.dart';
import '../../services/office/pptx_parser.dart';
import '../../theme/app_theme.dart';
import '../ui/page_nav_shortcuts.dart';

/// Unified native Office viewer + ink annotation (PPTX / DOCX).
class OfficeDocumentScreen extends StatefulWidget {
  const OfficeDocumentScreen({
    super.key,
    required this.filePath,
  });

  final String filePath;

  @override
  State<OfficeDocumentScreen> createState() => _OfficeDocumentScreenState();
}

class _OfficeDocumentScreenState extends State<OfficeDocumentScreen> {
  bool _loading = true;
  String? _error;
  ParsedPptx? _pptx;
  ParsedDocx? _docx;
  int _pageIndex = 0;
  final List<_InkStroke> _strokes = [];
  _InkStroke? _live;
  final TransformationController _transform = TransformationController();

  String get _sidecarPath => '${widget.filePath}.badnote.json';

  @override
  void initState() {
    super.initState();
    _open();
  }

  @override
  void dispose() {
    _transform.dispose();
    super.dispose();
  }

  Future<void> _open() async {
    final ext = p.extension(widget.filePath).toLowerCase();
    try {
      if (ext == '.pptx' || ext == '.ppt') {
        _pptx = await PptxParser().parse(widget.filePath);
      } else if (ext == '.docx') {
        _docx = await DocxParser().parse(widget.filePath);
      } else {
        throw StateError('Unsupported: $ext');
      }
      await _loadSidecar();
      BadNoteLog.instance.info(LogSubsystem.office, 'office_open', fields: {
        'path': widget.filePath,
        'pages': pageCount,
      });
    } catch (e) {
      _error = '$e';
      BadNoteLog.instance.error(LogSubsystem.office, 'office_open_failed', fields: {
        'error': '$e',
      });
    }
    if (mounted) setState(() => _loading = false);
  }

  int get pageCount {
    if (_pptx != null) return _pptx!.slides.length;
    if (_docx != null) return (_docx!.blocks.length / 12).ceil().clamp(1, 9999);
    return 0;
  }

  Future<void> _loadSidecar() async {
    final f = File(_sidecarPath);
    if (!await f.exists()) return;
    try {
      final json = jsonDecode(await f.readAsString()) as Map<String, dynamic>;
      final pages = json['pages'] as Map<String, dynamic>? ?? {};
      final key = '$_pageIndex';
      final list = pages[key] as List<dynamic>? ?? [];
      _strokes
        ..clear()
        ..addAll(list.map((e) => _InkStroke.fromJson(e as Map<String, dynamic>)));
    } catch (_) {}
  }

  Future<void> _saveSidecar() async {
    Map<String, dynamic> root = {'version': 1, 'pages': <String, dynamic>{}};
    final f = File(_sidecarPath);
    if (await f.exists()) {
      try {
        root = jsonDecode(await f.readAsString()) as Map<String, dynamic>;
      } catch (_) {}
    }
    final pages = (root['pages'] as Map<String, dynamic>?) ?? {};
    pages['$_pageIndex'] = _strokes.map((s) => s.toJson()).toList();
    root['pages'] = pages;
    await f.writeAsString(const JsonEncoder.withIndent('  ').convert(root));
  }

  Future<void> _goPage(int i) async {
    await _saveSidecar();
    setState(() {
      _pageIndex = i.clamp(0, pageCount - 1);
      _strokes.clear();
      _live = null;
    });
    await _loadSidecar();
    if (mounted) setState(() {});
  }

  void _onPointerDown(PointerDownEvent e) {
    if (e.kind != ui.PointerDeviceKind.stylus &&
        e.kind != ui.PointerDeviceKind.invertedStylus &&
        e.kind != ui.PointerDeviceKind.mouse) {
      return;
    }
    final local = _toScene(e.localPosition);
    _live = _InkStroke(points: [local], pressures: [e.pressure]);
    PenEventRing.instance.recordPointer(
      kind: 'down',
      pointerId: e.pointer,
      deviceKind: e.kind.name,
      pressure: e.pressure,
      decision: 'draw',
    );
    setState(() {});
  }

  void _onPointerMove(PointerMoveEvent e) {
    final live = _live;
    if (live == null) return;
    live.points.add(_toScene(e.localPosition));
    live.pressures.add(e.pressure);
    setState(() {});
  }

  void _onPointerUp(PointerUpEvent e) {
    final live = _live;
    if (live == null) return;
    setState(() {
      _strokes.add(live);
      _live = null;
    });
    unawaited(_saveSidecar());
  }

  Offset _toScene(Offset local) {
    final inv = Matrix4.inverted(_transform.value);
    return MatrixUtils.transformPoint(inv, local);
  }

  @override
  Widget build(BuildContext context) {
    if (_loading) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }
    if (_error != null) {
      return Scaffold(
        appBar: AppBar(title: Text(p.basename(widget.filePath))),
        body: Center(child: Text(_error!)),
      );
    }

    return pageNavShortcuts(
      onPrevious: _pageIndex > 0 ? () => _goPage(_pageIndex - 1) : null,
      onNext:
          _pageIndex < pageCount - 1 ? () => _goPage(_pageIndex + 1) : null,
      onFirst: pageCount > 0 ? () => _goPage(0) : null,
      onLast: pageCount > 0 ? () => _goPage(pageCount - 1) : null,
      child: Scaffold(
        appBar: AppBar(
          title: Text(p.basename(widget.filePath)),
          actions: [
            IconButton(
              onPressed: _pageIndex > 0 ? () => _goPage(_pageIndex - 1) : null,
              icon: const Icon(Icons.chevron_left),
            ),
            Center(child: Text('${_pageIndex + 1} / $pageCount')),
            IconButton(
              onPressed: _pageIndex < pageCount - 1
                  ? () => _goPage(_pageIndex + 1)
                  : null,
              icon: const Icon(Icons.chevron_right),
            ),
          ],
        ),
        body: InteractiveViewer(
          transformationController: _transform,
          minScale: 0.5,
          maxScale: 4,
          child: Listener(
            onPointerDown: _onPointerDown,
            onPointerMove: _onPointerMove,
            onPointerUp: _onPointerUp,
            child: CustomPaint(
              painter: _OfficePagePainter(
                pptx: _pptx,
                docx: _docx,
                pageIndex: _pageIndex,
                strokes: _strokes,
                live: _live,
              ),
              size: _pageSize,
            ),
          ),
        ),
      ),
    );
  }

  Size get _pageSize {
    if (_pptx != null && _pptx!.slides.isNotEmpty) {
      final s = _pptx!.slides[_pageIndex.clamp(0, _pptx!.slides.length - 1)];
      return Size(s.width, s.height);
    }
    return const Size(800, 1100);
  }
}

class _InkStroke {
  _InkStroke({required this.points, required this.pressures});

  final List<Offset> points;
  final List<double> pressures;

  Map<String, dynamic> toJson() => {
        'points': [
          for (final p in points) {'x': p.dx, 'y': p.dy},
        ],
        'pressures': pressures,
      };

  factory _InkStroke.fromJson(Map<String, dynamic> json) {
    final pts = (json['points'] as List<dynamic>)
        .map((e) => Offset(
              (e['x'] as num).toDouble(),
              (e['y'] as num).toDouble(),
            ))
        .toList();
    final pr = (json['pressures'] as List<dynamic>?)
            ?.map((e) => (e as num).toDouble())
            .toList() ??
        List.filled(pts.length, 0.5);
    return _InkStroke(points: pts, pressures: pr);
  }
}

class _OfficePagePainter extends CustomPainter {
  _OfficePagePainter({
    required this.pptx,
    required this.docx,
    required this.pageIndex,
    required this.strokes,
    required this.live,
  });

  final ParsedPptx? pptx;
  final ParsedDocx? docx;
  final int pageIndex;
  final List<_InkStroke> strokes;
  final _InkStroke? live;

  @override
  void paint(Canvas canvas, Size size) {
    final bg = Paint()..color = AppTokens.paper;
    canvas.drawRect(Offset.zero & size, bg);

    if (pptx != null && pptx!.slides.isNotEmpty) {
      final slide = pptx!.slides[pageIndex.clamp(0, pptx!.slides.length - 1)];
      final border = Paint()
        ..color = AppTokens.rule
        ..style = PaintingStyle.stroke;
      canvas.drawRect(Offset.zero & Size(slide.width, slide.height), border);
      for (final run in slide.runs) {
        final tp = TextPainter(
          text: TextSpan(
            text: run.text,
            style: TextStyle(
              color: AppTokens.ink,
              fontSize: run.fontSize,
            ),
          ),
          textDirection: TextDirection.ltr,
        )..layout(maxWidth: run.width > 0 ? run.width : slide.width - 96);
        tp.paint(canvas, Offset(run.x, run.y));
      }
    } else if (docx != null) {
      final start = pageIndex * 12;
      final blocks = docx!.blocks.skip(start).take(12).toList();
      var y = 48.0;
      for (final b in blocks) {
        final style = TextStyle(
          color: AppTokens.ink,
          fontSize: b.type == DocBlockType.heading ? 22 - b.level * 2.0 : 15,
          fontWeight:
              b.type == DocBlockType.heading ? FontWeight.w700 : FontWeight.w400,
        );
        final tp = TextPainter(
          text: TextSpan(text: b.text, style: style),
          textDirection: TextDirection.ltr,
        )..layout(maxWidth: size.width - 96);
        tp.paint(canvas, Offset(48, y));
        y += tp.height + 12;
      }
    }

    final ink = Paint()
      ..color = AppTokens.copper
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.2
      ..strokeCap = StrokeCap.round
      ..strokeJoin = StrokeJoin.round;
    for (final s in [...strokes, if (live != null) live!]) {
      if (s.points.length < 2) continue;
      final path = Path()..moveTo(s.points.first.dx, s.points.first.dy);
      for (var i = 1; i < s.points.length; i++) {
        path.lineTo(s.points[i].dx, s.points[i].dy);
      }
      canvas.drawPath(path, ink);
    }
  }

  @override
  bool shouldRepaint(covariant _OfficePagePainter oldDelegate) => true;
}
