import 'dart:math' as math;
import 'dart:typed_data';

import 'package:flutter/services.dart' show rootBundle;
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;

import 'file_service.dart';

/// How the labels are tiled on the page: many small stickers to peel onto
/// packets, or fewer larger cards for jars and boxes.
enum LabelSheetFormat { stickers, cards }

/// One printable seed label. All strings arrive already localized — this
/// service knows nothing about i18n or the domain. [qrData] is the opaque
/// payload for the QR (built by the caller via `SeedLabelCodec`).
class LabelSheetLabel {
  const LabelSheetLabel({
    required this.varietyLabel,
    required this.qrData,
    this.commonName,
    this.scientificName,
    this.details,
  });

  /// The variety's label, as its keeper writes it ("Acelga de Perales").
  final String varietyLabel;

  /// The species' common name in the keeper's locale, shown uppercase on top
  /// ("ACELGA"), like the old hand-written labels. Null when unknown.
  final String? commonName;

  /// Scientific name of the linked species (rendered in italics), if any.
  final String? scientificName;

  /// The batch facts line ("2006 · Perales · 2 cobs"), if any.
  final String? details;

  /// The QR payload (a `tanemaki://seed` URI), so the seed's data rides on the
  /// physical packet.
  final String qrData;
}

/// Fixed page geometry per [LabelSheetFormat], in PDF points.
class _Geometry {
  const _Geometry({
    required this.columns,
    required this.rowHeight,
    required this.qr,
    required this.common,
    required this.title,
    required this.small,
  });

  final int columns;
  final double rowHeight;
  final double qr;
  final double common;
  final double title;
  final double small;
}

/// Renders a sheet of seed labels (each with a QR) as a PDF and hands it to the
/// user via the save dialog — a paper bridge that needs no network. Pure of
/// platform channels apart from loading the bundled font, so it runs (and is
/// tested) on the host.
///
/// The embedded DejaVu face covers Latin (extended), Cyrillic and Greek. Like
/// the OCR traineddata, the PDF font is a per-locale resource: wider scripts
/// (Arabic, CJK) ship alongside their locales, not hardcoded here.
class LabelSheetService {
  LabelSheetService({required FileService files}) : _files = files;

  final FileService _files;

  /// Outer page margin (points, ~8.5 mm).
  static const _margin = 24.0;

  static _Geometry _geometryOf(LabelSheetFormat format) => switch (format) {
    // ~3 columns of ~64 mm stickers, tight text.
    LabelSheetFormat.stickers => const _Geometry(
      columns: 3,
      rowHeight: 104,
      qr: 46,
      common: 7,
      title: 9,
      small: 6.5,
    ),
    // ~2 columns of ~96 mm cards, room for the full facts line.
    LabelSheetFormat.cards => const _Geometry(
      columns: 2,
      rowHeight: 152,
      qr: 82,
      common: 9,
      title: 13,
      small: 9,
    ),
  };

  Future<Uint8List> buildPdf({
    required List<LabelSheetLabel> labels,
    required LabelSheetFormat format,
    bool rtl = false,
  }) async {
    final textDirection = rtl ? pw.TextDirection.rtl : pw.TextDirection.ltr;
    final base = pw.Font.ttf(
      await rootBundle.load('assets/fonts/DejaVuSans.ttf'),
    );
    final bold = pw.Font.ttf(
      await rootBundle.load('assets/fonts/DejaVuSans-Bold.ttf'),
    );
    final doc = pw.Document(
      theme: pw.ThemeData.withFont(base: base, bold: bold, italic: base),
    );

    final g = _geometryOf(format);
    final labelWidth = (PdfPageFormat.a4.width - _margin * 2) / g.columns;

    pw.Widget labelBox(LabelSheetLabel label) => pw.Container(
      width: labelWidth,
      height: g.rowHeight,
      padding: const pw.EdgeInsets.all(6),
      decoration: pw.BoxDecoration(
        border: pw.Border.all(color: PdfColors.grey400, width: 0.5),
      ),
      child: pw.Row(
        crossAxisAlignment: pw.CrossAxisAlignment.center,
        children: [
          pw.BarcodeWidget(
            barcode: pw.Barcode.qrCode(),
            data: label.qrData,
            width: g.qr,
            height: g.qr,
            drawText: false,
          ),
          pw.SizedBox(width: 6),
          pw.Expanded(
            // Only the label's text mirrors for RTL; the grid itself stays
            // left-to-right (a physical sheet has no reading direction).
            child: pw.Directionality(
              textDirection: textDirection,
              child: pw.Column(
                crossAxisAlignment: pw.CrossAxisAlignment.start,
                mainAxisAlignment: pw.MainAxisAlignment.center,
                mainAxisSize: pw.MainAxisSize.min,
                children: [
                  if (label.commonName != null)
                    pw.Text(
                      label.commonName!.toUpperCase(),
                      maxLines: 1,
                      overflow: pw.TextOverflow.clip,
                      style: pw.TextStyle(
                        font: bold,
                        fontSize: g.common,
                        color: PdfColors.grey800,
                      ),
                    ),
                  pw.Text(
                    label.varietyLabel,
                    maxLines: 2,
                    overflow: pw.TextOverflow.clip,
                    style: pw.TextStyle(font: bold, fontSize: g.title),
                  ),
                  if (label.scientificName != null)
                    pw.Text(
                      label.scientificName!,
                      maxLines: 1,
                      overflow: pw.TextOverflow.clip,
                      style: pw.TextStyle(
                        fontSize: g.small,
                        fontStyle: pw.FontStyle.italic,
                        color: PdfColors.grey700,
                      ),
                    ),
                  if (label.details != null)
                    pw.Text(
                      label.details!,
                      maxLines: 2,
                      overflow: pw.TextOverflow.clip,
                      style: pw.TextStyle(fontSize: g.small),
                    ),
                ],
              ),
            ),
          ),
        ],
      ),
    );

    // One MultiPage child per grid row, so pagination breaks cleanly between
    // rows and never splits a label. The last row is padded with empty cells to
    // keep the grid left-aligned.
    final rows = <pw.Widget>[];
    for (var i = 0; i < labels.length; i += g.columns) {
      final end = math.min(i + g.columns, labels.length);
      rows.add(
        pw.Row(
          crossAxisAlignment: pw.CrossAxisAlignment.start,
          children: [
            for (var j = i; j < end; j++) labelBox(labels[j]),
            for (var k = end - i; k < g.columns; k++)
              pw.SizedBox(width: labelWidth),
          ],
        ),
      );
    }

    doc.addPage(
      pw.MultiPage(
        pageFormat: PdfPageFormat.a4,
        margin: const pw.EdgeInsets.all(_margin),
        build: (context) => rows,
      ),
    );
    return doc.save();
  }

  /// Builds the sheet and asks the user where to keep it. Returns true when
  /// saved, false when they cancelled the dialog.
  Future<bool> saveLabels({
    required List<LabelSheetLabel> labels,
    required LabelSheetFormat format,
    required String suggestedName,
    bool rtl = false,
  }) async {
    final bytes = await buildPdf(labels: labels, format: format, rtl: rtl);
    final path = await _files.saveFile(
      suggestedName: suggestedName,
      bytes: bytes,
    );
    return path != null;
  }
}
