import 'dart:io';

import 'package:bcrypt/bcrypt.dart';
import 'package:dotenv/dotenv.dart';
import 'package:postgres/postgres.dart';
import 'package:uuid/uuid.dart';

import '../models/user.dart';
import '../models/geoposition.dart';
import '../models/log.dart';

class DatabaseProvider {
  late Connection _dbConnection;

  final Map<String, String> _shareLinks = {};
  final _uuid = const Uuid();

  Future<void> initialize() async {
    final dotenv = DotEnv();

    final host = dotenv['POSTGRES_HOST'] ?? 'db';
    final port = int.parse(dotenv['POSTGRES_PORT'] ?? '5432');
    final databaseName = dotenv['POSTGRES_DB'] ?? 'family_safety';
    final username = dotenv['POSTGRES_USER'] ?? 'postgres';
    final password = dotenv['POSTGRES_PASSWORD'] ?? 'postgres';

    try {
      final defaultConnection = await Connection.open(
        settings: ConnectionSettings(sslMode: SslMode.disable),
        Endpoint(
          host: host,
          port: port,
          database: 'postgres',
          username: username,
          password: password,
        ),
      );

      final results = await defaultConnection.execute(
        Sql.named('SELECT 1 FROM pg_database WHERE datname = @dbName'),
        parameters: {'dbName': databaseName},
      );

      if (results.isEmpty) {
        await defaultConnection.execute(
          Sql('CREATE DATABASE $databaseName'),
        );
        print('Database $databaseName created.');
      } else {
        print('Database $databaseName already exists.');
      }

      await defaultConnection.close();

      _dbConnection = await Connection.open(
        settings: ConnectionSettings(sslMode: SslMode.disable),
        Endpoint(
          host: host,
          port: port,
          database: databaseName,
          username: username,
          password: password,
        ),
      );
      print('Connected to database $databaseName.');

      await _dbConnection.execute(
        Sql.named('''
        CREATE TABLE IF NOT EXISTS users (
          id SERIAL PRIMARY KEY,
          login VARCHAR(255) UNIQUE NOT NULL,
          pwd_hash VARCHAR(255) NOT NULL
        )
        '''),
      );

      await _dbConnection.execute(
        Sql.named('''
        CREATE TABLE IF NOT EXISTS geopositions (
          id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
          x_value DOUBLE PRECISION NOT NULL,
          y_value DOUBLE PRECISION NOT NULL,
          last_update TIMESTAMP NOT NULL DEFAULT NOW(),
          expires_at TIMESTAMP NOT NULL
        )
        '''),
      );

      await _dbConnection.execute(
        Sql.named('''
        CREATE TABLE IF NOT EXISTS logs (
          id SERIAL PRIMARY KEY,
          username VARCHAR(255) NOT NULL,
          action VARCHAR(255) NOT NULL,
          datetime TIMESTAMP NOT NULL DEFAULT NOW()
        )
        '''),
      );

      print('All tables ensured to exist.');
    } catch (e, stackTrace) {
      stderr.writeln('Database initialization error: $e');
      stderr.writeln(stackTrace);
      exit(1);
    }
  }

  Future<void> close() async {
    await _dbConnection.close();
  }

  // ==================== User operations ====================

  Future<User?> findUserByLogin(String login) async {
    final results = await _dbConnection.execute(
      Sql.named('SELECT id, login, pwd_hash FROM users WHERE login = @login'),
      parameters: {'login': login},
    );

    if (results.isEmpty) return null;
    final row = results.first;
    return User(
      id: int.parse(row[0].toString()),
      login: row[1] as String,
      pwdHash: row[2] as String,
    );
  }

  Future<User> createUser(String login, String password) async {
    final hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt());

    final results = await _dbConnection.execute(
      Sql.named('''
      INSERT INTO users (login, pwd_hash) VALUES (@login, @pwdHash)
      RETURNING id, login, pwd_hash
      '''),
      parameters: {
        'login': login,
        'pwdHash': hashedPassword,
      },
    );

    final row = results.first;
    return User(
      id: int.parse(row[0].toString()),
      login: row[1] as String,
      pwdHash: row[2] as String,
    );
  }

  Future<void> deleteUser(int id) async {
    await _dbConnection.execute(
      Sql.named('DELETE FROM users WHERE id = @id'),
      parameters: {'id': id},
    );
  }

  Future<User> updateUser(int id, String newLogin, String? newPassword) async {
    if (newPassword != null) {
      final hashedPassword = BCrypt.hashpw(newPassword, BCrypt.gensalt());
      await _dbConnection.execute(
        Sql.named('''
        UPDATE users SET login = @login, pwd_hash = @pwdHash WHERE id = @id
        '''),
        parameters: {
          'login': newLogin,
          'pwdHash': hashedPassword,
          'id': id,
        },
      );
    } else {
      await _dbConnection.execute(
        Sql.named('UPDATE users SET login = @login WHERE id = @id'),
        parameters: {
          'login': newLogin,
          'id': id,
        },
      );
    }

    final user = await findUserByLogin(newLogin);
    return user!;
  }

  Future<List<User>> getAllUsers() async {
    final results = await _dbConnection.execute(
      Sql.named('SELECT id, login, pwd_hash FROM users'),
    );

    return results
        .map(
          (row) => User(
            id: int.parse(row[0].toString()),
            login: row[1] as String,
            pwdHash: row[2] as String,
          ),
        )
        .toList();
  }

  // ==================== Geoposition operations ====================

  Future<Geoposition> createPosition(
    double x,
    double y,
  ) async {
    final expiresAt = DateTime.now().add(const Duration(hours: 24));

    final results = await _dbConnection.execute(
      Sql.named('''
      INSERT INTO geopositions (x_value, y_value, last_update, expires_at)
      VALUES (@xValue, @yValue, NOW(), @expiresAt)
      RETURNING id, x_value, y_value, last_update, expires_at
      '''),
      parameters: {
        'xValue': x,
        'yValue': y,
        'expiresAt': expiresAt.toIso8601String(),
      },
    );

    final row = results.first;
    return Geoposition(
      id: row[0].toString(),
      xValue: double.parse(row[1].toString()),
      yValue: double.parse(row[2].toString()),
      lastUpdate: DateTime.parse(row[3].toString()),
      expiresAt: DateTime.parse(row[4].toString()),
    );
  }

  Future<Geoposition> updatePosition(
    String id,
    double x,
    double y,
  ) async {
    final expiresAt = DateTime.now().add(const Duration(hours: 24));

    final results = await _dbConnection.execute(
      Sql.named('''
      UPDATE geopositions
      SET x_value = @xValue, y_value = @yValue, last_update = NOW(), expires_at = @expiresAt
      WHERE id = @id
      RETURNING id, x_value, y_value, last_update, expires_at
      '''),
      parameters: {
        'id': id,
        'xValue': x,
        'yValue': y,
        'expiresAt': expiresAt.toIso8601String(),
      },
    );

    final row = results.first;
    return Geoposition(
      id: row[0].toString(),
      xValue: double.parse(row[1].toString()),
      yValue: double.parse(row[2].toString()),
      lastUpdate: DateTime.parse(row[3].toString()),
      expiresAt: DateTime.parse(row[4].toString()),
    );
  }

Future<Geoposition?> getLatestPosition() async {
    final results = await _dbConnection.execute(
      Sql.named('''
      SELECT id, x_value, y_value, last_update, expires_at
      FROM geopositions
      WHERE expires_at > NOW()
      ORDER BY last_update DESC
      LIMIT 1
      '''),
    );

    if (results.isEmpty) return null;

    final row = results.first;
    return Geoposition(
      id: row[0].toString(),
      xValue: double.parse(row[1].toString()),
      yValue: double.parse(row[2].toString()),
      lastUpdate: DateTime.parse(row[3].toString()),
      expiresAt: DateTime.parse(row[4].toString()),
    );
  }

  Future<Geoposition?> getPositionById(String id) async {
    final results = await _dbConnection.execute(
      Sql.named('''
      SELECT id, x_value, y_value, last_update, expires_at
      FROM geopositions
      WHERE id = @id AND expires_at > NOW()
      '''),
      parameters: {'id': id},
    );

    if (results.isEmpty) return null;

    final row = results.first;
    return Geoposition(
      id: row[0].toString(),
      xValue: double.parse(row[1].toString()),
      yValue: double.parse(row[2].toString()),
      lastUpdate: DateTime.parse(row[3].toString()),
      expiresAt: DateTime.parse(row[4].toString()),
    );
  }

  Future<void> cleanupExpired() async {
    await _dbConnection.execute(
      Sql.named('DELETE FROM geopositions WHERE expires_at < NOW()'),
    );
  }

  // ==================== Share operations ====================

  String createShareId(String geoId) {
    final uniqueId = _uuid.v4();
    _shareLinks[uniqueId] = geoId;
    return uniqueId;
  }

  String? getGeoIdByShareId(String uniqueId) {
    return _shareLinks[uniqueId];
  }

  // ==================== Log operations ====================

  Future<void> createLog(String username, String action) async {
    await _dbConnection.execute(
      Sql.named('''
      INSERT INTO logs (username, action) VALUES (@username, @action)
      '''),
      parameters: {
        'username': username,
        'action': action,
      },
    );
  }

  Future<List<Log>> getAllLogs() async {
    final results = await _dbConnection.execute(
      Sql.named('SELECT id, username, action, datetime FROM logs'),
    );

    return results
        .map(
          (row) => Log(
            id: int.parse(row[0].toString()),
            username: row[1] as String,
            action: row[2] as String,
            datetime: DateTime.parse(row[3].toString()),
          ),
        )
        .toList();
  }

  }