import 'dart:convert';

import 'package:postgres/postgres.dart';

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

final class PostgresShareRepository implements ShareRepository {
  const PostgresShareRepository(this.connection);

  final Connection connection;

  @override
  Future<void> insertShare({
    required Share share,
    required List<ShareRecipient> recipients,
  }) async {
    await connection.execute(
      Sql.named('''
        INSERT INTO shares (
          id, sender_user_id, resource_type, payload_json, created_at,
          revoked_at
        )
        VALUES (
          @id::uuid, @sender_user_id::uuid, @resource_type,
          @payload_json::jsonb, @created_at, @revoked_at
        )
      '''),
      parameters: {
        'id': share.id,
        'sender_user_id': share.senderUserId,
        'resource_type': share.resourceType.wireName,
        'payload_json': jsonEncode(share.payloadJson),
        'created_at': share.createdAt,
        'revoked_at': share.revokedAt,
      },
    );

    for (final recipient in recipients) {
      await connection.execute(
        Sql.named('''
          INSERT INTO share_recipients (
            id, share_id, recipient_user_id, status, responded_at
          )
          VALUES (
            @id::uuid, @share_id::uuid, @recipient_user_id::uuid, @status,
            @responded_at
          )
          ON CONFLICT (share_id, recipient_user_id) DO NOTHING
        '''),
        parameters: {
          'id': recipient.id,
          'share_id': recipient.shareId,
          'recipient_user_id': recipient.recipientUserId,
          'status': recipient.status.wireName,
          'responded_at': recipient.respondedAt,
        },
      );
    }
  }

  @override
  Future<List<ShareInboxItem>> listInbox(String recipientUserId) async {
    final result = await connection.execute(
      Sql.named('''
        SELECT
          s.id AS share_id,
          s.sender_user_id,
          s.resource_type,
          s.payload_json,
          s.created_at,
          s.revoked_at,
          sr.id AS recipient_id,
          sr.recipient_user_id,
          sr.status,
          sr.responded_at
        FROM share_recipients sr
        INNER JOIN shares s ON s.id = sr.share_id
        WHERE sr.recipient_user_id = @recipient_user_id::uuid
        ORDER BY s.created_at DESC, s.id DESC
      '''),
      parameters: {'recipient_user_id': recipientUserId},
    );
    return [
      for (final row in result)
        _inboxItemFromValues(row.toColumnMap() as Map<String, Object?>),
    ];
  }

  @override
  Future<Share?> findShareById(String shareId) async {
    final result = await connection.execute(
      Sql.named('''
        SELECT id, sender_user_id, resource_type, payload_json, created_at,
               revoked_at
        FROM shares
        WHERE id = @id::uuid
        LIMIT 1
      '''),
      parameters: {'id': shareId},
    );
    return result.isEmpty
        ? null
        : _shareFromValues(result.single.toColumnMap() as Map<String, Object?>);
  }

  @override
  Future<ShareRecipient?> findRecipient({
    required String shareId,
    required String recipientUserId,
  }) async {
    final result = await connection.execute(
      Sql.named('''
        SELECT id, share_id, recipient_user_id, status, responded_at
        FROM share_recipients
        WHERE share_id = @share_id::uuid
          AND recipient_user_id = @recipient_user_id::uuid
        LIMIT 1
      '''),
      parameters: {'share_id': shareId, 'recipient_user_id': recipientUserId},
    );
    return result.isEmpty
        ? null
        : _recipientFromValues(
            result.single.toColumnMap() as Map<String, Object?>,
          );
  }

  @override
  Future<void> updateRecipientStatus({
    required String recipientId,
    required ShareRecipientStatus status,
    required DateTime respondedAt,
  }) async {
    await connection.execute(
      Sql.named('''
        UPDATE share_recipients
        SET status = @status, responded_at = @responded_at
        WHERE id = @id::uuid
      '''),
      parameters: {
        'id': recipientId,
        'status': status.wireName,
        'responded_at': respondedAt.toUtc(),
      },
    );
  }

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

ShareInboxItem _inboxItemFromValues(Map<String, Object?> values) {
  return ShareInboxItem(
    share: _shareFromValues(values),
    recipient: _recipientFromValues(values),
  );
}

Share _shareFromValues(Map<String, Object?> values) {
  return Share(
    id: _stringValue(values['share_id'] ?? values['id']),
    senderUserId: _stringValue(values['sender_user_id']),
    resourceType: SyncedResourceType.parse(
      _stringValue(values['resource_type']),
    ),
    payloadJson: _payloadValue(values['payload_json']),
    createdAt: _dateTimeValue(values['created_at']),
    revokedAt: _nullableDateTimeValue(values['revoked_at']),
  );
}

ShareRecipient _recipientFromValues(Map<String, Object?> values) {
  return ShareRecipient(
    id: _stringValue(values['recipient_id'] ?? values['id']),
    shareId: _stringValue(values['share_id']),
    recipientUserId: _stringValue(values['recipient_user_id']),
    status: ShareRecipientStatus.parse(_stringValue(values['status'])),
    respondedAt: _nullableDateTimeValue(values['responded_at']),
  );
}

Map<String, Object?> _payloadValue(Object? value) {
  if (value is Map<String, Object?>) {
    return value;
  }
  if (value is Map) {
    return Map<String, Object?>.from(value);
  }
  if (value is String) {
    final decoded = jsonDecode(value);
    if (decoded is Map<String, Object?>) {
      return decoded;
    }
    if (decoded is Map) {
      return Map<String, Object?>.from(decoded);
    }
  }
  throw const FormatException('Expected JSON object payload.');
}

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();
}
