import 'dart:convert';
import 'package:intl/intl.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import '../models/models.dart';

class RepoService<T> {
  static int dbVersion = 8;
  static String dbName = 'olivamiapos01.db';
  static const String id = Basico.identificadorWeb;
  static const String idLocal = Basico.identificadorLocal;
  static Database? _db;
  final Map<String, dynamic> contexto = {
    'Categoria': CategoriaProducto(),
    'Producto': Producto(),
    'Pedido': Pedido(productos: []),
    'PedidoProducto': PedidoProducto(),
    'Gasto': Gasto(),
    'Deposito': Deposito(),
    'CorteCaja': CorteCaja(),
  };

  Future<Database?> get db async {
    if (_db != null) return _db;

    _db = await databaseInit();
    return _db;
  }

  Future<Database> databaseInit() async {
    String dir = (await getApplicationDocumentsDirectory()).path;
    String path = join(dir, dbName);
    return await openDatabase(
      path,
      version: dbVersion,
      onCreate: _onCreate,
      onUpgrade: _onUpgrade,
    );
  }

  _onCreate(Database db, int version) async {
    contexto.forEach((String nombre, dynamic modelo) async {
      Map<String, dynamic> model = json.decode(json.encode(modelo.toJson()));
      String sql =
          "CREATE TABLE ${modelo.runtimeType.toString()} (id INTEGER PRIMARY KEY AUTOINCREMENT";
      model.forEach((String key, dynamic value) {
        if (key != "id") {
          String tipo = value.runtimeType.toString();
          String sqlType = "";
          if (equals(tipo, 'int')) {
            sqlType = "INTEGER";
          } else if (equals(tipo, 'double')) {
            sqlType = "REAL";
          } else if (equals(tipo, 'bool')) {
            sqlType = "BOOLEAN";
          } else {
            sqlType = "TEXT";
          }

          sql += ", $key $sqlType";
        }
      });
      sql += ")";
      await db.execute(sql);

      await db.execute('''
        CREATE TABLE PedidoProductoTopping (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          idPedidoProducto INTEGER,
          idTopping INTEGER,
          FOREIGN KEY (idPedidoProducto) REFERENCES PedidoProducto(id),
          FOREIGN KEY (idTopping) REFERENCES Producto(id)
        )
        ''');

      if (modelo.runtimeType.toString() == 'Pedido') {
        await db.execute(
            'ALTER TABLE Pedido ADD COLUMN descuento INTEGER DEFAULT 0');
      }

      await db.execute('''
    CREATE TABLE Descuento (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      porcentaje INTEGER
    )
  ''');

      await db.insert('Descuento', {'porcentaje': 0});
      await db.insert('Descuento', {'porcentaje': 5});
      await db.insert('Descuento', {'porcentaje': 10});
      await db.insert('Descuento', {'porcentaje': 15});
      await db.insert('Descuento', {'porcentaje': 20});
      await db.insert('Descuento', {'porcentaje': 25});
      await db.insert('Descuento', {'porcentaje': 30});
    });

    await db.execute('''
      CREATE TABLE ProductoTopping (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        idProducto INTEGER,
        idTopping INTEGER,
        FOREIGN KEY (idProducto) REFERENCES Producto(id),
        FOREIGN KEY (idTopping) REFERENCES Producto(id)
      )
    ''');

    await db.execute('''
      CREATE TABLE Variable (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        nombre TEXT,
        clave TEXT,
        descripcion TEXT,
        activo BOOLEAN,
        idLocal INTEGER,
        eliminado TEXT
      )
    ''');
  }

  void _onUpgrade(Database db, int oldVersion, int newVersion) async {
    while (oldVersion < newVersion) {
      switch (oldVersion) {
        case 1:
          await db.execute(
              "ALTER TABLE CategoriaProducto ADD COLUMN esToping INTEGER DEFAULT 0");

          await db.execute(
              "ALTER TABLE CategoriaProducto ADD COLUMN descripcion TEXT DEFAULT ''");

          await db.execute(
              "ALTER TABLE CategoriaProducto ADD COLUMN maximo INTEGER");

          break;

        case 2:
          await db.execute('''
            CREATE TABLE ProductoTopping (
              id INTEGER PRIMARY KEY AUTOINCREMENT,
              idProducto INTEGER,
              idTopping INTEGER,
              FOREIGN KEY (idProducto) REFERENCES Producto(id),
              FOREIGN KEY (idTopping) REFERENCES Producto(id)
            )
          ''');
          break;

        case 3:
          await db.execute('''
            CREATE TABLE PedidoProductoTopping (
              id INTEGER PRIMARY KEY AUTOINCREMENT,
              idPedidoProducto INTEGER,
              idTopping INTEGER,
              FOREIGN KEY (idPedidoProducto) REFERENCES PedidoProducto(id),
              FOREIGN KEY (idTopping) REFERENCES Producto(id)
            )
          ''');
          break;

        case 4:
          await db.execute('''
          ALTER TABLE Pedido ADD COLUMN descuento INTEGER DEFAULT 0
        ''');
          break;

        case 5:
          await db.execute('''
            CREATE TABLE IF NOT EXISTS Descuento (
              id INTEGER PRIMARY KEY AUTOINCREMENT,
              porcentaje INTEGER
            )
            ''');

          await db.insert('Descuento', {'porcentaje': 0});
          await db.insert('Descuento', {'porcentaje': 5});
          await db.insert('Descuento', {'porcentaje': 10});
          await db.insert('Descuento', {'porcentaje': 15});
          await db.insert('Descuento', {'porcentaje': 20});
          await db.insert('Descuento', {'porcentaje': 25});
          await db.insert('Descuento', {'porcentaje': 30});
          break;

        case 6:
          await db.execute('''
          ALTER TABLE Pedido ADD COLUMN tipoPago TEXT DEFAULT '';
        ''');
          await db.execute('''
          ALTER TABLE Pedido ADD COLUMN cantEfectivo REAL DEFAULT 0;
        ''');
          await db.execute('''
          ALTER TABLE Pedido ADD COLUMN cantTarjeta REAL DEFAULT 0;
        ''');
          await db.execute('''
          ALTER TABLE Pedido ADD COLUMN cantTransferencia REAL DEFAULT 0;
        ''');
          break;

        case 7:
          await db.execute('''
            CREATE TABLE Variable (
              id INTEGER PRIMARY KEY AUTOINCREMENT,
              nombre TEXT,
              clave TEXT,
              descripcion TEXT,
              activo BOOLEAN,
              idLocal INTEGER,
              eliminado TEXT
            )
          ''');
          break;
      }
      oldVersion++;
    }
  }

  Future<int> guardar(T model) async {
    try {
      var dbClient = await db;
      String nombreTabla = model.runtimeType.toString();
      String modelo = json.encode(model, toEncodable: toEncodable);
      Map<String, dynamic> modelMap = json.decode(modelo);

      int id = 0;
      if (modelMap['id'] != null && modelMap['id'] != 0) {
        await dbClient!.update(
          nombreTabla,
          modelMap,
          where: 'id = ?',
          whereArgs: [modelMap['id']],
        );
        id = modelMap['id'];
      } else {
        modelMap.remove('id');
        id = await dbClient!.insert(nombreTabla, modelMap);
      }

      if (model is Producto) {
        await _guardarToppings(dbClient, id, model.topings);
      }

      return id;
    } catch (e) {
      print('Error al guardar en $T: $e');
      return 0;
    }
  }

  Future<void> _guardarToppings(
      Database db, int idProducto, List<Producto>? topings) async {
    await db.delete('ProductoTopping',
        where: 'idProducto = ?', whereArgs: [idProducto]);

    if (topings != null) {
      for (var topping in topings) {
        await db.insert('ProductoTopping', {
          'idProducto': idProducto,
          'idTopping': topping.id,
        });
      }
    }
  }

  Future<int> guardarLocal(T model) async {
    var dbClient = await db;
    String nombreTabla = model.runtimeType.toString();
    String modelo = json.encode(model, toEncodable: toEncodable);
    Map<String, dynamic> modelMap = json.decode(modelo);

    if (nombreTabla == "PedidoProductoTopping") {
      modelMap.remove('idLocal');
      modelMap.remove('eliminado');
    }

    if (modelMap['id'] == null || modelMap['id'] == 0) {
      modelMap.remove('id');
    }

    int resultado;
    int? identificadorLocal = modelMap[Basico.identificadorLocal];

    if (identificadorLocal != null && identificadorLocal > 0) {
      resultado = await dbClient!.update(
        nombreTabla,
        modelMap,
        where: "$idLocal = ?",
        whereArgs: [identificadorLocal],
      );
    } else {
      resultado = await dbClient!.insert(nombreTabla, modelMap);
      var rawQuery =
          await dbClient.rawQuery("SELECT last_insert_rowid() as id");
      if (rawQuery.isNotEmpty) {
        resultado = int.parse(rawQuery.first["id"].toString());
        modelMap[Basico.identificadorLocal] = resultado;
      }
    }

    if (model is Pedido) {
      model.id = resultado;
    }

    if (model is Producto) {
      await _guardarToppings(dbClient!, model.id!, model.topings);
    }

    return resultado;
  }

  dynamic toEncodable(dynamic item) {
    if (item is Pedido) {
      return item.toJson();
    } else if (item is PedidoProducto) {
      return item.toJson();
    } else if (item is PedidoProductoTopping) {
      return item.toJson();
    } else if (item is Producto) {
      return item.toJson();
    } else if (item is CategoriaProducto) {
      return item.toJson();
    } else if (item is Variable) {
      return item.toJson();
    }
    throw UnsupportedError(
        'Type not supported for serialization: ${item.runtimeType}');
  }

  Future<List<T>> obtenerTodos({String orderBy = 'id DESC'}) async {
    var db = await this.db;
    String tableName = T.toString();
    var result = await db!.query(tableName, orderBy: orderBy);
    return result.map((map) => fromMap<T>(map)).toList();
  }

  T fromMap<T>(Map<String, dynamic> map) {
    switch (T) {
      case Pedido:
        return Pedido.fromJson(map) as T;
      case Producto:
        return Producto.fromJson(map) as T;
      default:
        throw Exception('Tipo no soportado');
    }
  }

  Future<Pedido?> obtenerPorId(int id) async {
    Database? dbClient = await db;
    List<Map> maps =
        await dbClient!.query('Pedido', where: 'id = ?', whereArgs: [id]);
    if (maps.isNotEmpty) {
      return Pedido.fromJson(Map<String, dynamic>.from(maps.first));
    }
    return null;
  }

  Future<List<PedidoProducto>> obtenerPorIdPedido(int idPedido) async {
    Database? dbClient = await db;
    List<Map> maps = await dbClient!
        .query('PedidoProducto', where: 'idPedido = ?', whereArgs: [idPedido]);
    return maps
        .map((map) => PedidoProducto.fromJson(Map<String, dynamic>.from(map)))
        .toList();
  }

  Future<int> contarPedidos() async {
    Database? dbClient = await db;
    var result = await dbClient!.rawQuery('SELECT COUNT(*) FROM Pedido');
    return Sqflite.firstIntValue(result) ?? 0;
  }

  Future<List<Pedido>> obtenerPedidosPaginados(int limit, int offset) async {
    Database? dbClient = await db;
    List<Map<String, dynamic>> result = await dbClient!
        .query('Pedido', limit: limit, offset: offset, orderBy: 'id DESC');
    return result.map((map) => Pedido.fromJson(map)).toList();
  }

  Future<Producto?> obtenerProductoPorId(int idProducto) async {
    Database? dbClient = await db;
    List<Map> maps = await dbClient!
        .query('Producto', where: 'id = ?', whereArgs: [idProducto]);
    if (maps.isNotEmpty) {
      return Producto.fromJson(Map<String, dynamic>.from(maps.first));
    }
    return null;
  }

  Future<List<int>> obtenerToppingsPorProducto(int idProducto) async {
    var dbClient = await db;
    var result = await dbClient!.query(
      'ProductoTopping',
      where: 'idProducto = ?',
      whereArgs: [idProducto],
    );
    return result.map((map) => map['idTopping'] as int).toList();
  }

  Future<List<PedidoProductoTopping>> obtenerToppingsPorPedidoProducto(
      int idPedidoProducto) async {
    var dbClient = await db;
    List<Map> maps = await dbClient!.query('PedidoProductoTopping',
        where: 'idPedidoProducto = ?', whereArgs: [idPedidoProducto]);

    if (maps.isNotEmpty) {
      return maps
          .map((map) =>
              PedidoProductoTopping.fromJson(Map<String, dynamic>.from(map)))
          .toList();
    }
    return [];
  }

  Future<int> obtenerProximoFolio() async {
    var dbClient = await db;
    var result = await dbClient!.rawQuery(
        'SELECT MAX(CAST(folio AS INTEGER)) as last_folio FROM Pedido');
    if (result.isNotEmpty && result.first["last_folio"] != null) {
      return int.tryParse(result.first["last_folio"].toString())! + 1;
    }
    return 1;
  }

  Future<List<T>> buscarPorFolio(String folio) async {
    var dbClient = await db;
    List<Map<String, dynamic>> maps = await dbClient!.query(
      'Pedido',
      where: 'folio LIKE ?',
      whereArgs: ['%$folio%'],
    );
    return maps.map((map) => fromMap<T>(map)).toList();
  }

  Future<List<Pedido>> buscarPorFecha(
      DateTime startDate, DateTime endDate) async {
    var dbClient = await db;

    String startDateString =
        DateFormat('yyyy-MM-dd 00:00:00').format(startDate);
    String endDateString = DateFormat('yyyy-MM-dd 23:59:59').format(endDate);

    List<Map<String, dynamic>> maps = await dbClient!.rawQuery('''
    SELECT * FROM Pedido 
    WHERE 
      (datetime(substr(peticion, 7, 4) || '-' || substr(peticion, 4, 2) || '-' || substr(peticion, 1, 2) || ' ' || substr(peticion, 12)) BETWEEN ? AND ?)
    OR 
      (datetime(substr(peticion, 7, 4) || '-' || substr(peticion, 1, 2) || '-' || substr(peticion, 4, 2) || ' ' || substr(peticion, 12)) BETWEEN ? AND ?)
  ''', [startDateString, endDateString, startDateString, endDateString]);

    return maps.map((map) => Pedido.fromJson(map)).toList();
  }

  Future<List<CorteCaja>> buscarPorFechaCorte(
      DateTime startDate, DateTime endDate) async {
    var dbClient = await db;
    String startDateString = DateFormat('dd-MM-yyyy').format(startDate);
    String endDateString = DateFormat('dd-MM-yyyy 23:59:59').format(endDate);

    List<Map<String, dynamic>> maps = await dbClient!.query(
      'CorteCaja',
      where: 'fecha BETWEEN ? AND ?',
      whereArgs: [startDateString, endDateString],
    );
    return maps.map((map) => CorteCaja.fromJson(map)).toList();
  }

  Future<void> eliminar<T>(int id) async {
    var dbClient = await db;
    String tableName = T.toString();
    await dbClient!.delete(tableName, where: 'id = ?', whereArgs: [id]);
  }

  Future<List<Descuento>> obtenerTodosDescuentos() async {
    var dbClient = await db;
    var result = await dbClient!.query('Descuento', orderBy: 'porcentaje ASC');
    return result.map((map) => Descuento.fromJson(map)).toList();
  }

  Future<int> guardarDescuento(Descuento descuento) async {
    var dbClient = await db;
    if (descuento.id != null && descuento.id! > 0) {
      return await dbClient!.update(
        'Descuento',
        descuento.toJson(),
        where: 'id = ?',
        whereArgs: [descuento.id],
      );
    } else {
      return await dbClient!.insert('Descuento', descuento.toJson());
    }
  }

  Future<int> eliminarDescuento(int id) async {
    var dbClient = await db;
    return await dbClient!
        .delete('Descuento', where: 'id = ?', whereArgs: [id]);
  }
}
