import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:niogu_app/core/database/app_database.dart';
import 'package:niogu_app/core/database/tables/outlets.dart';
import 'package:niogu_app/core/database/tables/sync_queues.dart';
import 'package:niogu_app/core/database/tables/tenants.dart';
import 'package:niogu_app/core/database/tables/users.dart';
import 'package:niogu_app/core/entities/custom_activity.dart';
import 'package:niogu_app/core/entities/custom_sync_queue.dart';
import 'package:niogu_app/core/enums/table_type.dart';
import 'package:niogu_app/core/models/outlet_model.dart';
import 'package:niogu_app/core/models/staff_model.dart';
import 'package:niogu_app/core/models/tenant_model.dart';
import 'package:niogu_app/core/models/user_model.dart';
import 'package:niogu_app/core/utils/bcrypt.dart';
import 'package:niogu_app/core/utils/extension_format.dart';
import 'package:niogu_app/core/enums/message_activity_type.dart';
import 'package:niogu_app/core/enums/sync_status.dart';
import 'package:niogu_app/core/system/system_setting.dart';
import 'package:niogu_app/core/utils/log_message.dart';
import 'package:niogu_app/core/utils/time_zone.dart';
import 'package:niogu_app/core/enums/user_role.dart';
import 'package:niogu_app/features/profile/domain/entities/profile.dart';
import 'package:niogu_app/features/user/domain/entities/user.dart';

part 'user_dao.g.dart';

@DriftAccessor(tables: [Tenants, Outlets, Users, SyncQueues])
class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
  UserDao(AppDatabase db) : super(db);

  Future<void> login({
    required TenantModel tenantModel,
    required List<OutletModel> outletModels,
    required UserModel userModel,
    required List<StaffModel> staffModels,
    String? localLogoPath,
    required Map<String, String?> localBannerPaths,
    required String password,
  }) async {
    try {
      await transaction(() async {
        await batch((batch) {
          final tenantComp = tenantModel.toDriftCompanion(
            localLogoPath: localLogoPath,
          );

          batch.insert(
            tenants,
            tenantComp,
            onConflict: DoUpdate((_) => tenantComp),
          );

          for (final outletModel in outletModels) {
            final localBannerPath = localBannerPaths[outletModel.serverId];

            final outletComp = outletModel.toDriftCompanion(
              localBannerPath: localBannerPath,
            );

            batch.insert(
              outlets,
              outletComp,
              onConflict: DoUpdate((_) => outletComp),
            );
          }

          final userComp = userModel.toDriftCompanion(
            Bcrypt.hashPassword(password),
          );

          batch.insert(users, userComp, onConflict: DoUpdate((_) => userComp));

          for (final staff in staffModels) {
            final staffComp = staff.toDriftCompanion();
            batch.insert(
              users,
              staffComp,
              onConflict: DoUpdate((_) => staffComp),
            );
          }
        });
      });
    } catch (e, st) {
      LogMessage.log.e(e.toString(), error: e, stackTrace: st);
      rethrow;
    }
  }

  Stream<ShiftInfo> watchShiftInfo() async* {
    final currentUserId = await SystemSetting.getCurrentUserId();

    final query = selectOnly(users)
      ..addColumns([users.shiftName, users.shiftStartTime, users.shiftEndTime])
      ..where(users.localId.equals(currentUserId!))
      ..limit(1);

    yield* query.watchSingle().map((row) {
      return ShiftInfo(
        shiftName: row.read(users.shiftName)!,
        shiftStartTime: row.read(users.shiftStartTime)!,
        shiftEndTime: row.read(users.shiftEndTime)!,
      );
    });
  }

  Stream<double> watchProfileCompleteness() async* {
    final currentUserId = await SystemSetting.getCurrentUserId();

    final query = selectOnly(users)
      ..addColumns([users.name, users.placeOfBirth, users.dateOfBirth])
      ..where(users.localId.equals(currentUserId!))
      ..limit(1);

    yield* query.watchSingle().map((row) {
      double percent = 0.6;

      final placeOfBirth = row.read(users.placeOfBirth);

      final dateOfBirth = row.read(users.dateOfBirth);

      if (placeOfBirth != null && placeOfBirth.isNotEmpty) {
        percent += 0.2;
      }

      if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
        percent += 0.2;
      }

      return percent;
    });
  }

  Future<ProfileInfo> getProfileInfo() async {
    final currentUserId = await SystemSetting.getCurrentUserId();

    final query = selectOnly(users)
      ..addColumns([
        users.name,
        users.email,
        users.phoneNumber,
        users.placeOfBirth,
        users.dateOfBirth,
      ])
      ..where(users.localId.equals(currentUserId!))
      ..limit(1);

    final result = await query.getSingle();

    return ProfileInfo(
      name: result.read(users.name) ?? '',
      email: result.read(users.email) ?? '',
      phoneNumber: result.read(users.phoneNumber) ?? '',
      placeOfBirth: result.read(users.placeOfBirth),
      dateOfBirth: result.read(users.dateOfBirth),
    );
  }

  Future<int> updateProfile(UpsertProfile profile) async {
    return await transaction(() async {
      final now = DateTime.now();

      final currentUserId = await SystemSetting.getCurrentUserId();

      final rowsAffected =
          (await update(users)
                ..where((users) => users.localId.equals(currentUserId!)))
              .write(
                UsersCompanion(
                  name: Value(profile.name),
                  placeOfBirth: Value(profile.placeOfBirth),
                  dateOfBirth: Value(profile.dateOfBirth),
                  syncStatus: Value(SyncStatus.dirty.status),
                  updatedAt: Value(now),
                ),
              );

      final payload = SyncPayload(
        table: TableType.users.type,
        localId: currentUserId!,
        action: SyncAction.update.action,
        data: {
          'uuid': currentUserId,
          'name': profile.name,
          'place_of_birth': profile.placeOfBirth,
          'date_of_birth': profile.dateOfBirth,
          'created_at': now.toIso8601String(),
          'updated_at': now.toIso8601String(),
        },
      ).toJson();

      final syncQueueComp = SyncQueuesCompanion(
        tableType: Value(TableType.users.type),
        tableLocalId: Value(currentUserId),
        action: Value(SyncAction.update_or_create.action),
        status: Value(SyncStatus.dirty.status),
        payload: Value(payload),
      );

      final existing =
          await (select(syncQueues)..where(
                (q) =>
                    q.tableType.equals(TableType.users.type) &
                    q.tableLocalId.equals(currentUserId),
              ))
              .getSingleOrNull();

      if (existing != null) {
        await (update(
          syncQueues,
        )..where((q) => q.id.equals(existing.id))).write(syncQueueComp);
      } else {
        await into(syncQueues).insert(syncQueueComp);
      }

      return rowsAffected;
    });
  }

  Future<bool> validatePassword(String inputPassword) async {
    final currentUserId = await SystemSetting.getCurrentUserId();

    final query = selectOnly(users)
      ..addColumns([users.passwordHash])
      ..where(users.localId.equals(currentUserId!))
      ..limit(1);

    final result = await query.getSingle();

    final passwordHash = result.read(users.passwordHash)!;

    return Bcrypt.verifyPassword(inputPassword, passwordHash);
  }

  Future<int> updatePassword(String newPassword) async {
    return await transaction(() async {
      final now = DateTime.now();

      final currentUserId = await SystemSetting.getCurrentUserId();

      final rowsAffected =
          (await update(users)
                ..where((user) => user.localId.equals(currentUserId!)))
              .write(
                UsersCompanion(
                  passwordHash: Value(Bcrypt.hashPassword(newPassword)),
                  syncStatus: Value(SyncStatus.dirty.status),
                  updatedAt: Value(now),
                ),
              );

      final payload = SyncPayload(
        table: TableType.users.type,
        localId: currentUserId!,
        action: SyncAction.update.action,
        data: {
          'uuid': currentUserId,
          'password_hash': Bcrypt.hashPassword(newPassword),
          'created_at': now.toIso8601String(),
          'updated_at': now.toIso8601String(),
        },
      ).toJson();

      final syncQueueComp = SyncQueuesCompanion(
        tableType: Value(TableType.users.type),
        tableLocalId: Value(currentUserId),
        action: Value(SyncAction.update_or_create.action),
        status: Value(SyncStatus.dirty.status),
        payload: Value(payload),
      );

      final existing =
          await (select(syncQueues)..where(
                (q) =>
                    q.tableType.equals(TableType.users.type) &
                    q.tableLocalId.equals(currentUserId),
              ))
              .getSingleOrNull();

      if (existing != null) {
        await (update(
          syncQueues,
        )..where((q) => q.id.equals(existing.id))).write(syncQueueComp);
      } else {
        await into(syncQueues).insert(syncQueueComp);
      }

      return rowsAffected;
    });
  }

  TimeOfDay _parseTimeOfDay(String timeString) {
    List<String> parts = timeString.split('.');

    int hour = int.parse(parts[0]);
    int minute = int.parse(parts[1]);

    return TimeOfDay(hour: hour, minute: minute);
  }

  Stream<MessageActivity?> checkPermissionActivity() async* {
    final userId = await SystemSetting.getCurrentUserId();

    final userRole = await SystemSetting.getCurrentUserRole();

    final outletId = await SystemSetting.getCurrentOutletId();

    final query =
        selectOnly(
            outlets,
          ).join([innerJoin(users, users.outletId.equalsExp(outlets.localId))])
          ..addColumns([
            outlets.name,
            outlets.isActive,
            users.isActive,
            users.shiftName,
            users.shiftStartTime,
            users.shiftEndTime,
            outlets.deletedAt,
            users.deletedAt,
          ])
          ..orderBy([OrderingTerm.asc(users.createdAt)])
          ..where(outlets.localId.equals(outletId!))
          ..where(
            userRole == UserRole.admin
                ? users.localId.equals(userId!)
                : const Constant(true),
          )
          ..limit(1);
    yield* query.watch().map((rows) {
      final currentTime = TimeOfDay.now();

      for (final row in rows) {
        final String name = row.read(outlets.name) ?? '';

        final bool isOutletDeleted = row.read(outlets.deletedAt) != null;

        if (isOutletDeleted)
          return MessageActivity(
            type: MessageActivityType.lock_activity,
            messages: ["Outlet ini ($name) telah di hapus"],
          );

        final bool isOutletActive = row.read(outlets.isActive) ?? false;

        if (!isOutletActive)
          return MessageActivity(
            type: MessageActivityType.lock_activity,
            messages: ["Outlet ini ($name) telah di non aktifkan / tutup"],
          );

        if (userRole == UserRole.owner) return null;

        final bool isUserDeleted = row.read(users.deletedAt) != null;

        if (isUserDeleted)
          return MessageActivity(
            type: MessageActivityType.lock_activity,
            messages: ["Anda telah diberhetikan sebagai admin"],
          );

        final bool isUserActive = row.read(outlets.isActive) ?? false;

        if (!isUserActive)
          return MessageActivity(
            type: MessageActivityType.lock_activity,
            messages: ["Anda di non aktifkan sementara oleh pemilik"],
          );

        final String shiftName = row.read(users.shiftName) ?? '';

        final TimeOfDay shiftStartTime = _parseTimeOfDay(
          (row.read(users.shiftStartTime) ?? ''),
        );

        final TimeOfDay shiftEndTime = _parseTimeOfDay(
          (row.read(users.shiftEndTime) ?? ''),
        );

        final hasShift =
            currentTime.isAfterOrEqual(shiftStartTime) &&
            currentTime.isBefore(shiftEndTime);

        if (!hasShift)
          return MessageActivity(
            type: MessageActivityType.lock_activity,
            messages: [
              "Saat ini belum shift kerja anda, shift kerja anda ($shiftName) di pukul ${(row.read(users.shiftStartTime))} ${TimeZone.getCurrentTimeZone()} - ${(row.read(users.shiftEndTime))} ${TimeZone.getCurrentTimeZone()}",
            ],
          );
      }

      return null;
    });
  }
}
