import 'package:postgres/postgres.dart';

import '../../application/application.dart';
import '../../domain/domain.dart';

final class PostgresUserRepository implements UserRepository {
  const PostgresUserRepository(this.connection);

  final Connection connection;

  @override
  Future<UserAccount?> findByEmail(String email) async {
    final result = await connection.execute(
      Sql.named('''
        SELECT id, email, password_hash, display_name, created_at, updated_at,
               disabled_at
        FROM users
        WHERE email = @email
        LIMIT 1
      '''),
      parameters: {'email': email.trim().toLowerCase()},
    );
    return result.isEmpty ? null : _userFromRow(result.single);
  }

  @override
  Future<UserAccount?> findById(String id) async {
    final result = await connection.execute(
      Sql.named('''
        SELECT id, email, password_hash, display_name, created_at, updated_at,
               disabled_at
        FROM users
        WHERE id = @id
        LIMIT 1
      '''),
      parameters: {'id': id},
    );
    return result.isEmpty ? null : _userFromRow(result.single);
  }

  @override
  Future<void> insert(UserAccount user) async {
    await connection.execute(
      Sql.named('''
        INSERT INTO users (
          id, email, password_hash, display_name, created_at, updated_at,
          disabled_at
        )
        VALUES (
          @id, @email, @password_hash, @display_name, @created_at,
          @updated_at, @disabled_at
        )
      '''),
      parameters: {
        'id': user.id,
        'email': user.email,
        'password_hash': user.passwordHash,
        'display_name': user.displayName,
        'created_at': user.createdAt,
        'updated_at': user.updatedAt,
        'disabled_at': user.disabledAt,
      },
    );
  }

  @override
  Future<void> updatePasswordHash({
    required String userId,
    required String passwordHash,
    required DateTime updatedAt,
  }) async {
    await connection.execute(
      Sql.named('''
        UPDATE users
        SET password_hash = @password_hash, updated_at = @updated_at
        WHERE id = @user_id
      '''),
      parameters: {
        'user_id': userId,
        'password_hash': passwordHash,
        'updated_at': updatedAt.toUtc(),
      },
    );
  }
}

final class PostgresAuthSessionRepository implements AuthSessionRepository {
  const PostgresAuthSessionRepository(this.connection);

  final Connection connection;

  @override
  Future<void> insert(AuthSession session) async {
    await connection.execute(
      Sql.named('''
        INSERT INTO auth_sessions (
          id, user_id, token_hash, issued_at, expires_at, revoked_at,
          user_agent, device_label
        )
        VALUES (
          @id, @user_id, @token_hash, @issued_at, @expires_at, @revoked_at,
          @user_agent, @device_label
        )
      '''),
      parameters: {
        'id': session.id,
        'user_id': session.userId,
        'token_hash': session.tokenHash,
        'issued_at': session.issuedAt,
        'expires_at': session.expiresAt,
        'revoked_at': session.revokedAt,
        'user_agent': session.userAgent,
        'device_label': session.deviceLabel,
      },
    );
  }

  @override
  Future<AuthSession?> findByTokenHash(String tokenHash) async {
    final result = await connection.execute(
      Sql.named('''
        SELECT id, user_id, token_hash, issued_at, expires_at, revoked_at,
               user_agent, device_label
        FROM auth_sessions
        WHERE token_hash = @token_hash
        LIMIT 1
      '''),
      parameters: {'token_hash': tokenHash},
    );
    return result.isEmpty ? null : _authSessionFromRow(result.single);
  }

  @override
  Future<void> revoke({
    required String sessionId,
    required DateTime revokedAt,
  }) async {
    await connection.execute(
      Sql.named('''
        UPDATE auth_sessions
        SET revoked_at = @revoked_at
        WHERE id = @id AND revoked_at IS NULL
      '''),
      parameters: {'id': sessionId, 'revoked_at': revokedAt.toUtc()},
    );
  }
}

UserAccount _userFromRow(dynamic row) {
  final values = row.toColumnMap() as Map<String, Object?>;
  return UserAccount(
    id: _stringValue(values['id']),
    email: _stringValue(values['email']),
    passwordHash: _stringValue(values['password_hash']),
    displayName: values['display_name'] as String?,
    createdAt: _dateTimeValue(values['created_at']),
    updatedAt: _dateTimeValue(values['updated_at']),
    disabledAt: _nullableDateTimeValue(values['disabled_at']),
  );
}

AuthSession _authSessionFromRow(dynamic row) {
  final values = row.toColumnMap() as Map<String, Object?>;
  return AuthSession(
    id: _stringValue(values['id']),
    userId: _stringValue(values['user_id']),
    tokenHash: _stringValue(values['token_hash']),
    issuedAt: _dateTimeValue(values['issued_at']),
    expiresAt: _dateTimeValue(values['expires_at']),
    revokedAt: _nullableDateTimeValue(values['revoked_at']),
    userAgent: values['user_agent'] as String?,
    deviceLabel: values['device_label'] as String?,
  );
}

String _stringValue(Object? value) {
  if (value == null) {
    throw const FormatException('Expected non-null string value.');
  }
  return value.toString();
}

DateTime _dateTimeValue(Object? value) {
  final result = _nullableDateTimeValue(value);
  if (result == null) {
    throw const FormatException('Expected non-null DateTime value.');
  }
  return result;
}

DateTime? _nullableDateTimeValue(Object? value) {
  if (value == null) {
    return null;
  }
  if (value is DateTime) {
    return value.toUtc();
  }
  return DateTime.parse(value.toString()).toUtc();
}
