import 'dart:convert';
import 'dart:typed_data';

import 'package:flutter/widgets.dart';

/// Read-only model for a ProseMirror / TipTap document node.
///
/// Parsing is deliberately total: an unknown `type` never throws, it becomes a
/// [PmUnknown] so the renderer can fall back gracefully (schemaVersion drift).
sealed class PmNode {
  const PmNode();

  /// Block/inline children. Empty for leaf nodes.
  List<PmNode> get children => const [];

  factory PmNode.fromJson(Map<String, dynamic> json) {
    final type = json['type'];
    final attrs = _attrs(json['attrs']);
    switch (type) {
      case 'paragraph':
        return PmParagraph(
          align: _parseTextAlign(attrs['textAlign']),
          children: _parseChildren(json['content']),
        );
      case 'heading':
        return PmHeading(
          level: _parseInt(attrs['level'], fallback: 1, min: 1, max: 6),
          align: _parseTextAlign(attrs['textAlign']),
          children: _parseChildren(json['content']),
        );
      case 'text':
        return PmText(
          text: json['text'] is String ? json['text'] as String : '',
          marks: _parseMarks(json['marks']),
        );
      case 'bulletList':
        return PmBulletList(children: _parseChildren(json['content']));
      case 'orderedList':
        return PmOrderedList(
          start: _parseInt(attrs['start'], fallback: 1, min: 1),
          children: _parseChildren(json['content']),
        );
      case 'listItem':
        return PmListItem(children: _parseChildren(json['content']));
      case 'blockquote':
        return PmBlockquote(children: _parseChildren(json['content']));
      case 'codeBlock':
        return PmCodeBlock(
          language: attrs['language'] is String
              ? attrs['language'] as String
              : null,
          children: _parseChildren(json['content']),
        );
      case 'horizontalRule':
        return const PmHorizontalRule();
      case 'hardBreak':
        return const PmHardBreak();
      case 'image':
        return PmImage.fromAttrs(attrs);
      case 'callout':
        return PmCallout(
          variant: PmCalloutVariant.parse(attrs['variant']),
          children: _parseChildren(json['content']),
        );
      case 'table':
        return PmTable(children: _parseChildren(json['content']));
      case 'tableRow':
        return PmTableRow(children: _parseChildren(json['content']));
      case 'tableHeader':
      case 'tableCell':
        return PmTableCell(
          header: type == 'tableHeader',
          colspan: _parseInt(attrs['colspan'], fallback: 1, min: 1),
          rowspan: _parseInt(attrs['rowspan'], fallback: 1, min: 1),
          children: _parseChildren(json['content']),
        );
      default:
        return PmUnknown(
          rawType: type is String ? type : 'unknown',
          children: _parseChildren(json['content']),
        );
    }
  }

  static List<PmNode> _parseChildren(dynamic content) {
    if (content is! List) return const [];
    return content
        .whereType<Map<dynamic, dynamic>>()
        .map((e) => PmNode.fromJson(e.cast<String, dynamic>()))
        .toList(growable: false);
  }

  static List<PmMark> _parseMarks(dynamic marks) {
    if (marks is! List) return const [];
    return marks
        .whereType<Map<dynamic, dynamic>>()
        .map((m) {
          final map = m.cast<String, dynamic>();
          final type = map['type'];
          return PmMark(
            type: type is String ? type : '',
            attrs: _attrs(map['attrs']),
          );
        })
        .where((m) => m.type.isNotEmpty)
        .toList(growable: false);
  }

  static Map<String, dynamic> _attrs(dynamic attrs) =>
      attrs is Map ? attrs.cast<String, dynamic>() : const {};

  static TextAlign? _parseTextAlign(dynamic value) {
    switch (value) {
      case 'left':
        return TextAlign.left;
      case 'right':
        return TextAlign.right;
      case 'center':
        return TextAlign.center;
      case 'justify':
        return TextAlign.justify;
      default:
        return null;
    }
  }

  static int _parseInt(
    dynamic value, {
    required int fallback,
    int? min,
    int? max,
  }) {
    var result = fallback;
    if (value is num) result = value.toInt();
    if (value is String) result = int.tryParse(value) ?? fallback;
    if (min != null && result < min) result = min;
    if (max != null && result > max) result = max;
    return result;
  }
}

/// A formatting mark on a [PmText] run (bold, link, highlight, …). Unknown mark
/// types are kept verbatim and simply ignored by the renderer.
class PmMark {
  final String type;
  final Map<String, dynamic> attrs;

  const PmMark({required this.type, this.attrs = const {}});
}

class PmParagraph extends PmNode {
  final TextAlign? align;
  @override
  final List<PmNode> children;

  const PmParagraph({this.align, this.children = const []});
}

class PmHeading extends PmNode {
  final int level;
  final TextAlign? align;
  @override
  final List<PmNode> children;

  const PmHeading({required this.level, this.align, this.children = const []});
}

class PmText extends PmNode {
  final String text;
  final List<PmMark> marks;

  const PmText({required this.text, this.marks = const []});
}

class PmBulletList extends PmNode {
  @override
  final List<PmNode> children;

  const PmBulletList({this.children = const []});
}

class PmOrderedList extends PmNode {
  final int start;
  @override
  final List<PmNode> children;

  const PmOrderedList({this.start = 1, this.children = const []});
}

class PmListItem extends PmNode {
  @override
  final List<PmNode> children;

  const PmListItem({this.children = const []});
}

class PmBlockquote extends PmNode {
  @override
  final List<PmNode> children;

  const PmBlockquote({this.children = const []});
}

class PmCodeBlock extends PmNode {
  final String? language;
  @override
  final List<PmNode> children;

  const PmCodeBlock({this.language, this.children = const []});
}

class PmHorizontalRule extends PmNode {
  const PmHorizontalRule();
}

class PmHardBreak extends PmNode {
  const PmHardBreak();
}

class PmImage extends PmNode {
  final String src;
  final String? alt;
  final String? title;
  final String? align;
  final String? width;
  final String? href;

  /// Base64 `data:image/` payloads are decoded exactly once at parse time and
  /// memoised here — never re-decoded in `build()`. `null` means either a
  /// network source or a decode failure (renderer shows a broken-image icon).
  final Uint8List? bytes;

  const PmImage({
    required this.src,
    this.alt,
    this.title,
    this.align,
    this.width,
    this.href,
    this.bytes,
  });

  factory PmImage.fromAttrs(Map<String, dynamic> attrs) {
    final src = attrs['src'] is String ? attrs['src'] as String : '';
    return PmImage(
      src: src,
      alt: attrs['alt'] is String ? attrs['alt'] as String : null,
      title: attrs['title'] is String ? attrs['title'] as String : null,
      align: attrs['align'] is String ? attrs['align'] as String : null,
      width: attrs['width'] is String ? attrs['width'] as String : null,
      href: attrs['href'] is String ? attrs['href'] as String : null,
      bytes: _decodeDataImage(src),
    );
  }

  static Uint8List? _decodeDataImage(String src) {
    if (!src.startsWith('data:image/')) return null;
    final comma = src.indexOf(',');
    if (comma < 0) return null;
    if (!src.substring(0, comma).contains(';base64')) return null;
    try {
      return base64Decode(src.substring(comma + 1));
    } catch (_) {
      return null;
    }
  }
}

enum PmCalloutVariant {
  info,
  tip,
  success,
  warning,
  important;

  static PmCalloutVariant parse(dynamic value) {
    for (final variant in PmCalloutVariant.values) {
      if (variant.name == value) return variant;
    }
    return PmCalloutVariant.info;
  }
}

class PmCallout extends PmNode {
  final PmCalloutVariant variant;
  @override
  final List<PmNode> children;

  const PmCallout({required this.variant, this.children = const []});
}

class PmTable extends PmNode {
  @override
  final List<PmNode> children;

  const PmTable({this.children = const []});
}

class PmTableRow extends PmNode {
  @override
  final List<PmNode> children;

  const PmTableRow({this.children = const []});
}

class PmTableCell extends PmNode {
  final bool header;
  final int colspan;
  final int rowspan;
  @override
  final List<PmNode> children;

  const PmTableCell({
    this.header = false,
    this.colspan = 1,
    this.rowspan = 1,
    this.children = const [],
  });
}

class PmUnknown extends PmNode {
  final String rawType;
  @override
  final List<PmNode> children;

  const PmUnknown({required this.rawType, this.children = const []});
}
