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

import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart';
import 'package:image/image.dart' as img;
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

import 'label_text_extractor.dart';

/// On-device OCR via Tesseract (offline, no cloud). Reads the **largest** text
/// on a packet/label photo — the variety name is the biggest print, not the
/// longest line — so boilerplate like "Organic Product" or "Herb Seeds" never
/// wins.
///
/// Security note: the plugin takes a file path, so the photo is written to a
/// private temp file and **deleted immediately** in `finally` — it never lives
/// as plaintext at rest beyond the OCR call. The bundled traineddata the plugin
/// copies to app storage is public reference data, not user content.
class TesseractLabelExtractor implements LabelTextExtractor {
  const TesseractLabelExtractor({
    this.language = 'eng+spa',
    this.rotations = const [0, 90, 180, 270, 45, 135, 225, 315],
  });

  /// Tesseract language(s), '+'-joined; needs matching bundled traineddata.
  final String language;

  /// Angles (degrees) to OCR the photo at, keeping the most confident read.
  /// Tesseract only reads axis-aligned text, so this makes a packet shot
  /// sideways, upside-down or ~45° tilted still work. More angles = slower
  /// (one OCR pass each); trim to `[0, 90, 180, 270]` if latency matters.
  final List<int> rotations;

  @override
  bool get isSupported => true;

  @override
  Future<String?> suggestLabel(Uint8List photo) async {
    final base = _preprocess(photo);
    if (base == null) return null;
    File? temp;
    try {
      final dir = await getTemporaryDirectory();
      temp = File(p.join(dir.path, 'ocr_${identityHashCode(photo)}.png'));
      HocrLabel? best;
      for (final angle in rotations) {
        final image = angle == 0 ? base : img.copyRotate(base, angle: angle);
        await temp.writeAsBytes(img.encodePng(image), flush: true);
        // hOCR carries per-word bounding boxes + confidence, so we can rank by
        // font size, drop low-confidence noise, and compare rotations.
        final hocr = await FlutterTesseractOcr.extractHocr(
          temp.path,
          language: language,
          args: {'preserve_interword_spaces': '1'},
        );
        final candidate = readHocrLabel(hocr);
        if (candidate != null &&
            (best == null || candidate.score > best.score)) {
          best = candidate;
        }
      }
      return best?.label;
    } on Object {
      return null;
    } finally {
      try {
        if (temp != null && await temp.exists()) await temp.delete();
      } on Object {
        // Best effort: nothing to recover if cleanup fails.
      }
    }
  }
}

/// Words in an hOCR document ignored as packet boilerplate (lower-cased,
/// accent-stripped compare). Kept deliberately small and generic.
const _boilerplate = <String>{
  'organic',
  'product',
  'products',
  'herb',
  'herbs',
  'seed',
  'seeds',
  'bio',
  'net',
  'weight',
  'wt',
  'www',
  'com',
  'gr',
  'grs',
  'grams',
  'variety',
  'quality',
};

/// Selects the variety name from Tesseract [hocr]: keep the words whose glyph
/// height is close to the tallest (the title print), drop boilerplate, and join
/// them in reading order (top-to-bottom, left-to-right). Returns null when
/// nothing usable remains. Pure — unit-tested without a native engine.
/// A label read from one hOCR pass, with a [score] (sum of the kept words'
/// confidences) used to compare rotations — the most confident wins.
class HocrLabel {
  const HocrLabel(this.label, this.score);
  final String label;
  final int score;
}

/// Thin wrapper returning just the label (see [readHocrLabel]).
String? pickLabelFromHocr(String hocr, {int minConfidence = 20}) =>
    readHocrLabel(hocr, minConfidence: minConfidence)?.label;

HocrLabel? readHocrLabel(String hocr, {int minConfidence = 20}) {
  final wordRe = RegExp(
    r"""class=['"]ocrx_word['"][^>]*\bbbox\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)(?:[^>]*?x_wconf\s+(\d+))?[^>]*>([^<]*)<""",
  );
  final words = <_Word>[];
  for (final m in wordRe.allMatches(hocr)) {
    final x0 = int.parse(m.group(1)!);
    final y0 = int.parse(m.group(2)!);
    final y1 = int.parse(m.group(4)!);
    // Missing confidence → treat as passing (some hOCR omits x_wconf).
    final confidence = m.group(5) == null ? 100 : int.parse(m.group(5)!);
    final text = _unescape(m.group(6)!).trim();
    if (text.isEmpty) continue;
    words.add(
      _Word(
        x0: x0,
        y0: y0,
        height: y1 - y0,
        text: text,
        confidence: confidence,
      ),
    );
  }
  if (words.isEmpty) return null;

  final maxHeight = words.map((w) => w.height).reduce(math.max);
  // Keep prominent words (>= 60% of the tallest) with ≥3 letters, above a low
  // confidence floor, that are not boilerplate. The length rule kills 1–2 char
  // OCR fragments (the "Re Ma" garbage) while keeping a plausibly-misread title
  // word like "Sucumber" (which the user just edits to "Cucumber").
  final kept = words
      .where(
        (w) =>
            w.height >= 0.6 * maxHeight &&
            w.confidence >= minConfidence &&
            _letterCount(w.text) >= 3 &&
            !_boilerplate.contains(_fold(w.text)),
      )
      .toList();
  if (kept.isEmpty) return null;

  // Reading order: group into rows by vertical position, then left-to-right.
  final band = maxHeight * 0.6;
  kept.sort((a, b) {
    if ((a.y0 - b.y0).abs() > band) return a.y0.compareTo(b.y0);
    return a.x0.compareTo(b.x0);
  });

  final chosen = kept.take(6).toList();
  final label = _titleCase(_clean(chosen.map((w) => w.text).join(' ')));
  if (label.isEmpty) return null;
  final score = chosen.fold<int>(0, (sum, w) => sum + w.confidence);
  return HocrLabel(label, score);
}

class _Word {
  const _Word({
    required this.x0,
    required this.y0,
    required this.height,
    required this.text,
    required this.confidence,
  });
  final int x0;
  final int y0;
  final int height;
  final String text;
  final int confidence;
}

/// Grayscale + contrast + upscale to help Tesseract on blurry/small photos.
/// Returns the decoded image (ready to rotate/encode), or null if it can't be
/// decoded.
img.Image? _preprocess(Uint8List bytes) {
  final decoded = img.decodeImage(bytes);
  if (decoded == null) return null;
  var image = decoded;
  // Upscale so glyphs are big enough for the LSTM engine.
  if (image.width < 1200) {
    image = img.copyResize(image, width: 1200);
  }
  image = img.grayscale(image);
  return img.adjustColor(image, contrast: 1.4);
}

/// Case- and accent-folded token for boilerplate matching.
String _fold(String s) {
  const from = 'áàäâãéèëêíìïîóòöôõúùüûñç';
  const to = 'aaaaaeeeeiiiiooooouuuunc';
  final lower = s.toLowerCase();
  final buffer = StringBuffer();
  for (final ch in lower.split('')) {
    final i = from.indexOf(ch);
    buffer.write(i == -1 ? ch : to[i]);
  }
  return buffer.toString().replaceAll(RegExp(r'[^a-z0-9]'), '');
}

int _letterCount(String s) =>
    RegExp(r'\p{L}', unicode: true).allMatches(s).length;

String _clean(String s) => s
    .replaceAll(RegExp(r"[^\p{L}\p{N}\s'-]", unicode: true), ' ')
    .replaceAll(RegExp(r'\s+'), ' ')
    .trim();

String _titleCase(String s) => s
    .toLowerCase()
    .split(' ')
    .map((w) => w.isEmpty ? w : '${w[0].toUpperCase()}${w.substring(1)}')
    .join(' ');

String _unescape(String s) => s
    .replaceAll('&amp;', '&')
    .replaceAll('&#39;', "'")
    .replaceAll('&quot;', '"')
    .replaceAll('&lt;', '<')
    .replaceAll('&gt;', '>');
