// GeoPackage (.gpkg) export — OGC szabvány, QGIS-kompatibilis
//
// Tartalom:
//   - note_items_point   : rögzített pontok
//   - note_items_line    : rögzített vonalak
//   - note_items_polygon : rögzített területek
//   - measured_points    : bemért pontok
//   - tracks             : GPS nyomvonalak
//
// Kimenet: ZIP archív (.gpkg + médiafájlok)

import 'dart:io';
import 'dart:typed_data';

import 'package:archive/archive_io.dart';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:sqflite/sqflite.dart';
import 'package:terepi_seged/models/measured_point.dart';
import 'package:terepi_seged/services/app_database.dart';
import 'package:terepi_seged/services/project_service.dart';

import '../models/note_item.dart';
import '../enums/note_type.dart';

class GeoPackageExporter {
  // ── Publikus belépési pont ────────────────────────────────────────

  Future<void> exportProject() async {
    final project = ProjectService.to.activeProject.value;
    final projectId = ProjectService.to.activeProjectId;
    final name = project?.name ?? 'projekt';

    final ts = DateFormat('yyyyMMdd_HHmm').format(DateTime.now());
    final tmpDir = await getTemporaryDirectory();
    final workDir = Directory(p.join(tmpDir.path, 'gpkg_export_$ts'));
    await workDir.create(recursive: true);

    try {
      // 1. GeoPackage létrehozása
      final gpkgPath = p.join(workDir.path, '$name.gpkg');
      await _buildGpkg(gpkgPath, projectId);

      // 2. Médiafájlok összegyűjtése
      final mediaDir = Directory(p.join(workDir.path, 'media'));
      await _collectMedia(projectId, mediaDir);

      // 3. ZIP csomagolás
      final zipPath = p.join(tmpDir.path, '${name}_$ts.zip');
      await _packZip(workDir, zipPath);

      // 4. Megosztás
      await SharePlus.instance.share(ShareParams(
        files: [XFile(zipPath, mimeType: 'application/zip')],
        subject: 'GeoPackage export — $name',
      ));
    } finally {
      // Ideiglenes munkamappa törlése
      await workDir.delete(recursive: true).catchError((_) {});
    }
  }

  // ── GeoPackage (.gpkg) létrehozása ───────────────────────────────

  Future<void> _buildGpkg(String path, int? projectId) async {
    final db = await openDatabase(path);
    try {
      await _initGpkg(db);
      await _exportNoteItems(db, projectId);
      await _exportMeasuredPoints(db, projectId);
      await _exportTracks(db, projectId);
    } finally {
      await db.close();
    }
  }

  // ── GeoPackage kötelező táblák ────────────────────────────────────

  Future<void> _initGpkg(Database db) async {
    // GeoPackage magic numbers
    await db.execute('PRAGMA application_id = 1196444487');
    await db.execute('PRAGMA user_version  = 10200');

    // Spatial reference systems
    await db.execute('''
      CREATE TABLE gpkg_spatial_ref_sys (
        srs_name                TEXT NOT NULL,
        srs_id                  INTEGER NOT NULL PRIMARY KEY,
        organization            TEXT NOT NULL,
        organization_coordsys_id INTEGER NOT NULL,
        definition              TEXT NOT NULL,
        description             TEXT
      )''');

    // WGS84 bejegyzés
    await db.insert('gpkg_spatial_ref_sys', {
      'srs_name': 'WGS 84 geographic 2D',
      'srs_id': 4326,
      'organization': 'EPSG',
      'organization_coordsys_id': 4326,
      'definition': 'GEOGCS["WGS 84",DATUM["WGS_1984",'
          'SPHEROID["WGS 84",6378137,298.257223563]],'
          'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]',
      'description': 'longitude/latitude coordinates in decimal degrees',
    });
    // Undefined srs
    await db.insert('gpkg_spatial_ref_sys', {
      'srs_name': 'Undefined Cartesian SRS',
      'srs_id': -1,
      'organization': 'NONE',
      'organization_coordsys_id': -1,
      'definition': 'undefined',
    });
    await db.insert('gpkg_spatial_ref_sys', {
      'srs_name': 'Undefined geographic SRS',
      'srs_id': 0,
      'organization': 'NONE',
      'organization_coordsys_id': 0,
      'definition': 'undefined',
    });

    // Contents tábla
    await db.execute('''
      CREATE TABLE gpkg_contents (
        table_name  TEXT NOT NULL PRIMARY KEY,
        data_type   TEXT NOT NULL,
        identifier  TEXT,
        description TEXT DEFAULT '',
        last_change DATETIME NOT NULL
            DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', CURRENT_TIMESTAMP)),
        min_x REAL, min_y REAL, max_x REAL, max_y REAL,
        srs_id INTEGER,
        CONSTRAINT fk_gc_r_srs_id
            FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)
      )''');

    // Geometry columns tábla
    await db.execute('''
      CREATE TABLE gpkg_geometry_columns (
        table_name        TEXT NOT NULL,
        column_name       TEXT NOT NULL,
        geometry_type_name TEXT NOT NULL,
        srs_id            INTEGER NOT NULL,
        z                 TINYINT NOT NULL,
        m                 TINYINT NOT NULL,
        CONSTRAINT pk_geom_cols PRIMARY KEY (table_name, column_name),
        CONSTRAINT fk_gc_tn
            FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name),
        CONSTRAINT fk_gc_srs
            FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)
      )''');
  }

  // ── Feature tábla regisztrálása ───────────────────────────────────

  Future<void> _registerLayer(
    Database db, {
    required String tableName,
    required String geomType, // 'POINT', 'LINESTRING', 'POLYGON'
    required String identifier,
    List<double>? extent, // [minX, minY, maxX, maxY]
  }) async {
    await db.insert('gpkg_contents', {
      'table_name': tableName,
      'data_type': 'features',
      'identifier': identifier,
      'description': '',
      'last_change': DateTime.now().toUtc().toIso8601String(),
      'min_x': extent?[0],
      'min_y': extent?[1],
      'max_x': extent?[2],
      'max_y': extent?[3],
      'srs_id': 4326,
    });
    await db.insert('gpkg_geometry_columns', {
      'table_name': tableName,
      'column_name': 'geom',
      'geometry_type_name': geomType,
      'srs_id': 4326,
      'z': 0,
      'm': 0,
    });
  }

  // ── NoteItem exportok ─────────────────────────────────────────────

  Future<void> _exportNoteItems(Database db, int? projectId) async {
    final items = await AppDatabase.instance.listNoteItems(projectId);
    final points = items.where((i) => i.type == NoteType.point).toList();
    final lines = items.where((i) => i.type == NoteType.line).toList();
    final polygons = items.where((i) => i.type == NoteType.polygon).toList();

    if (points.isNotEmpty) await _exportPoints(db, points);
    if (lines.isNotEmpty) await _exportLines(db, lines);
    if (polygons.isNotEmpty) await _exportPolygons(db, polygons);
  }

  Future<void> _exportPoints(Database db, List<NoteItem> items) async {
    await db.execute('''
      CREATE TABLE note_items_point (
        id          INTEGER PRIMARY KEY,
        geom        BLOB NOT NULL,
        label       TEXT,
        color_hex   TEXT,
        created_at  TEXT,
        photo_count INTEGER DEFAULT 0,
        audio_count INTEGER DEFAULT 0
      )''');
    await _registerLayer(db,
        tableName: 'note_items_point',
        geomType: 'POINT',
        identifier: 'Rögzített pontok',
        extent: _extent(items));

    for (final item in items) {
      final pt = item.points.first;
      final pc = await AppDatabase.instance.countNotePhotos(item.id!);
      final ac = await AppDatabase.instance.countNoteAudios(item.id!);
      await db.insert('note_items_point', {
        'id': item.id,
        'geom': _gpkgPoint(pt.longitude, pt.latitude),
        'label': item.label,
        'color_hex':
            '#${item.color.value.toRadixString(16).padLeft(8, '0').substring(2)}',
        'created_at': item.createdAt.toIso8601String(),
        'photo_count': pc,
        'audio_count': ac,
      });
    }
  }

  Future<void> _exportLines(Database db, List<NoteItem> items) async {
    await db.execute('''
      CREATE TABLE note_items_line (
        id           INTEGER PRIMARY KEY,
        geom         BLOB NOT NULL,
        label        TEXT,
        color_hex    TEXT,
        stroke_width REAL,
        point_count  INTEGER,
        created_at   TEXT,
        photo_count  INTEGER DEFAULT 0,
        audio_count  INTEGER DEFAULT 0
      )''');
    await _registerLayer(db,
        tableName: 'note_items_line',
        geomType: 'LINESTRING',
        identifier: 'Rögzített vonalak',
        extent: _extent(items));

    for (final item in items) {
      final pc = await AppDatabase.instance.countNotePhotos(item.id!);
      final ac = await AppDatabase.instance.countNoteAudios(item.id!);
      await db.insert('note_items_line', {
        'id': item.id,
        'geom': _gpkgLineString(item.points),
        'label': item.label,
        'color_hex':
            '#${item.color.value.toRadixString(16).padLeft(8, '0').substring(2)}',
        'stroke_width': item.strokeWidth,
        'point_count': item.points.length,
        'created_at': item.createdAt.toIso8601String(),
        'photo_count': pc,
        'audio_count': ac,
      });
    }
  }

  Future<void> _exportPolygons(Database db, List<NoteItem> items) async {
    await db.execute('''
      CREATE TABLE note_items_polygon (
        id           INTEGER PRIMARY KEY,
        geom         BLOB NOT NULL,
        label        TEXT,
        color_hex    TEXT,
        opacity      REAL,
        stroke_width REAL,
        point_count  INTEGER,
        created_at   TEXT,
        photo_count  INTEGER DEFAULT 0,
        audio_count  INTEGER DEFAULT 0
      )''');
    await _registerLayer(db,
        tableName: 'note_items_polygon',
        geomType: 'POLYGON',
        identifier: 'Rögzített területek',
        extent: _extent(items));

    for (final item in items) {
      final pc = await AppDatabase.instance.countNotePhotos(item.id!);
      final ac = await AppDatabase.instance.countNoteAudios(item.id!);
      await db.insert('note_items_polygon', {
        'id': item.id,
        'geom': _gpkgPolygon(item.points),
        'label': item.label,
        'color_hex':
            '#${item.color.value.toRadixString(16).padLeft(8, '0').substring(2)}',
        'opacity': item.opacity,
        'stroke_width': item.strokeWidth,
        'point_count': item.points.length,
        'created_at': item.createdAt.toIso8601String(),
        'photo_count': pc,
        'audio_count': ac,
      });
    }
  }

  // ── Bemért pontok exportja ────────────────────────────────────────

  Future<void> _exportMeasuredPoints(Database db, int? projectId) async {
    final points = projectId != null
        ? await AppDatabase.instance.listMeasuredPoints(projectId)
        : <MeasuredPoint>[];
    if (points.isEmpty) return;

    await db.execute('''
      CREATE TABLE measured_points (
        id           INTEGER PRIMARY KEY,
        geom         BLOB NOT NULL,
        name         TEXT,
        eov_y        REAL,
        eov_x        REAL,
        eov_z        REAL,
        accuracy     REAL,
        fix_quality  INTEGER,
        timestamp    TEXT,
        note         TEXT
      )''');
    await _registerLayer(db,
        tableName: 'measured_points',
        geomType: 'POINT',
        identifier: 'Bemért pontok');

    for (final pt in points) {
      if (pt.latitude == null || pt.longitude == null) continue;
      await db.insert('measured_points', {
        'id': pt.id,
        'geom': _gpkgPoint(pt.longitude!, pt.latitude!),
        'name': pt.name,
        'eov_y': pt.eovY,
        'eov_x': pt.eovX,
        'eov_z': pt.eovZ,
        'accuracy': pt.accuracy,
        'fix_quality': pt.fixQuality,
        'timestamp': pt.timestamp.toIso8601String(),
        'note': pt.note,
      });
    }
  }

  // ── Track exportja ────────────────────────────────────────────────

  Future<void> _exportTracks(Database db, int? projectId) async {
    final tracks = await AppDatabase.instance.listTracks();
    final filtered = projectId != null
        ? tracks.where((t) => t.projectId == projectId).toList()
        : tracks;
    if (filtered.isEmpty) return;

    await db.execute('''
      CREATE TABLE tracks (
        id               INTEGER PRIMARY KEY,
        geom             BLOB NOT NULL,
        name             TEXT,
        distance_meters  REAL,
        start_time       TEXT,
        end_time         TEXT,
        point_count      INTEGER
      )''');
    await _registerLayer(db,
        tableName: 'tracks',
        geomType: 'LINESTRING',
        identifier: 'GPS nyomvonalak');

    for (final track in filtered) {
      final pts = await AppDatabase.instance.getLatLons(track.id!);
      if (pts.length < 2) continue;

      // Egyszerű record list → LatLng-szerű párok
      final coords = pts.map((p) => (lon: p.lon, lat: p.lat)).toList();
      await db.insert('tracks', {
        'id': track.id,
        'geom': _gpkgLineStringFromRecords(coords),
        'name': track.name,
        'distance_meters': track.distanceMeters,
        'start_time': track.startTime.toIso8601String(),
        'end_time': track.endTime?.toIso8601String(),
        'point_count': pts.length,
      });
    }
  }

  // ── Médiafájlok másolása ──────────────────────────────────────────

  Future<void> _collectMedia(int? projectId, Directory mediaDir) async {
    if (projectId == null) return;

    final photos = Directory(p.join(mediaDir.path, 'photos'));
    final audios = Directory(p.join(mediaDir.path, 'audio'));
    await photos.create(recursive: true);
    await audios.create(recursive: true);

    final items = await AppDatabase.instance.listNoteItems(projectId);
    for (final item in items) {
      // Fotók
      final photoList = await AppDatabase.instance.listNotePhotos(item.id!);
      for (final photo in photoList) {
        final src = File(photo.localPath);
        if (await src.exists()) {
          final dst = File(p.join(photos.path, p.basename(photo.localPath)));
          await src.copy(dst.path);
        }
      }
      // Hangok
      final audioList = await AppDatabase.instance.listNoteAudios(item.id!);
      for (final audio in audioList) {
        final src = File(audio.localPath);
        if (await src.exists()) {
          final dst = File(p.join(audios.path, p.basename(audio.localPath)));
          await src.copy(dst.path);
        }
      }
    }
  }

  // ── ZIP csomagolás ────────────────────────────────────────────────

  Future<void> _packZip(Directory workDir, String zipPath) async {
    final archive = Archive();

    for (final entity in workDir.listSync(recursive: true)) {
      if (entity is! File) continue;
      final bytes = entity.readAsBytesSync();
      final rel = p.relative(entity.path, from: workDir.path);
      final archiveName = rel.replaceAll('\\', '/'); // Windows path fix
      archive.addFile(ArchiveFile(archiveName, bytes.length, bytes));
    }

    final encoded = ZipEncoder().encode(archive);
    if (encoded == null || encoded.isEmpty) {
      throw Exception(
          'ZIP encoding sikertelen — nincsenek exportálható adatok');
    }
    await File(zipPath).writeAsBytes(Uint8List.fromList(encoded));
  }

  // ── WKB/GeoPackage bináris kódolás ───────────────────────────────
  //
  // GPKG binary header (8 byte):
  //   0x47 0x50       – 'GP' magic
  //   0x00            – version 0
  //   0x01            – flags: little-endian, no envelope
  //   srs_id (int32)  – 4326 (WGS84)
  // + WKB geometry

  Uint8List _gpkgPoint(double lon, double lat) {
    final b = ByteData(8 + 21); // header + wkb point
    _writeGpkgHeader(b, 0);
    b.setUint8(8, 1); // WKB: little endian
    b.setUint32(9, 1, Endian.little); // type: Point
    b.setFloat64(13, lon, Endian.little);
    b.setFloat64(21, lat, Endian.little);
    return b.buffer.asUint8List();
  }

  Uint8List _gpkgLineString(List<dynamic> points) {
    final n = points.length;
    final b = ByteData(8 + 9 + n * 16);
    _writeGpkgHeader(b, 0);
    int o = 8;
    b.setUint8(o++, 1);
    b.setUint32(o, 2, Endian.little);
    o += 4; // type: LineString
    b.setUint32(o, n, Endian.little);
    o += 4;
    for (final pt in points) {
      b.setFloat64(o, pt.longitude, Endian.little);
      o += 8;
      b.setFloat64(o, pt.latitude, Endian.little);
      o += 8;
    }
    return b.buffer.asUint8List();
  }

  Uint8List _gpkgLineStringFromRecords(
      List<({double lon, double lat})> coords) {
    final n = coords.length;
    final b = ByteData(8 + 9 + n * 16);
    _writeGpkgHeader(b, 0);
    int o = 8;
    b.setUint8(o++, 1);
    b.setUint32(o, 2, Endian.little);
    o += 4;
    b.setUint32(o, n, Endian.little);
    o += 4;
    for (final c in coords) {
      b.setFloat64(o, c.lon, Endian.little);
      o += 8;
      b.setFloat64(o, c.lat, Endian.little);
      o += 8;
    }
    return b.buffer.asUint8List();
  }

  Uint8List _gpkgPolygon(List<dynamic> ring) {
    final n = ring.length;
    final b = ByteData(8 + 9 + 4 + n * 16);
    _writeGpkgHeader(b, 0);
    int o = 8;
    b.setUint8(o++, 1);
    b.setUint32(o, 3, Endian.little);
    o += 4; // type: Polygon
    b.setUint32(o, 1, Endian.little);
    o += 4; // 1 ring
    b.setUint32(o, n, Endian.little);
    o += 4;
    for (final pt in ring) {
      b.setFloat64(o, pt.longitude, Endian.little);
      o += 8;
      b.setFloat64(o, pt.latitude, Endian.little);
      o += 8;
    }
    return b.buffer.asUint8List();
  }

  void _writeGpkgHeader(ByteData b, int offset) {
    b.setUint8(offset, 0x47); // 'G'
    b.setUint8(offset + 1, 0x50); // 'P'
    b.setUint8(offset + 2, 0x00); // version
    b.setUint8(offset + 3, 0x01); // flags: little-endian, no envelope
    b.setInt32(offset + 4, 4326, Endian.little); // WGS84
  }

  // ── Segédek ───────────────────────────────────────────────────────

  /// Befoglaló téglalap az összes NoteItem pontjaiból
  List<double>? _extent(List<NoteItem> items) {
    final pts = items.expand((i) => i.points).toList();
    if (pts.isEmpty) return null;
    return [
      pts.map((p) => p.longitude).reduce((a, b) => a < b ? a : b),
      pts.map((p) => p.latitude).reduce((a, b) => a < b ? a : b),
      pts.map((p) => p.longitude).reduce((a, b) => a > b ? a : b),
      pts.map((p) => p.latitude).reduce((a, b) => a > b ? a : b),
    ];
  }
}
