import 'dart:convert';

import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:http/http.dart' as http;
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart' as domain;
import 'package:krow_domain/krow_domain.dart'
    show
        HubHasOrdersException,
        BusinessNotFoundException,
        NotAuthenticatedException;

import '../../domain/repositories/hub_repository_interface.dart';

/// Implementation of [HubRepositoryInterface] backed by Data Connect.
class HubRepositoryImpl implements HubRepositoryInterface {
  HubRepositoryImpl({
    required dc.DataConnectService service,
  }) : _service = service;

  final dc.DataConnectService _service;

  @override
  Future<List<domain.Hub>> getHubs() async {
    return _service.run(() async {
      final dc.GetBusinessesByUserIdBusinesses business = await _getBusinessForCurrentUser();
      final String teamId = await _getOrCreateTeamId(business);
      return _fetchHubsForTeam(teamId: teamId, businessId: business.id);
    });
  }

  @override
  Future<domain.Hub> createHub({
    required String name,
    required String address,
    String? placeId,
    double? latitude,
    double? longitude,
    String? city,
    String? state,
    String? street,
    String? country,
    String? zipCode,
  }) async {
    return _service.run(() async {
      final dc.GetBusinessesByUserIdBusinesses business = await _getBusinessForCurrentUser();
      final String teamId = await _getOrCreateTeamId(business);
      final _PlaceAddress? placeAddress =
          placeId == null || placeId.isEmpty ? null : await _fetchPlaceAddress(placeId);
      final String? cityValue = city ?? placeAddress?.city ?? business.city;
      final String? stateValue = state ?? placeAddress?.state;
      final String? streetValue = street ?? placeAddress?.street;
      final String? countryValue = country ?? placeAddress?.country;
      final String? zipCodeValue = zipCode ?? placeAddress?.zipCode;

      final OperationResult<dc.CreateTeamHubData, dc.CreateTeamHubVariables>
          result = await _service.connector
              .createTeamHub(
                teamId: teamId,
                hubName: name,
                address: address,
              )
              .placeId(placeId)
              .latitude(latitude)
              .longitude(longitude)
              .city(cityValue?.isNotEmpty == true ? cityValue : '')
              .state(stateValue)
              .street(streetValue)
              .country(countryValue)
              .zipCode(zipCodeValue)
              .execute();
      final String createdId = result.data.teamHub_insert.id;

      final List<domain.Hub> hubs = await _fetchHubsForTeam(
        teamId: teamId,
        businessId: business.id,
      );
      domain.Hub? createdHub;
      for (final domain.Hub hub in hubs) {
        if (hub.id == createdId) {
          createdHub = hub;
          break;
        }
      }
      return createdHub ??
          domain.Hub(
            id: createdId,
            businessId: business.id,
            name: name,
            address: address,
            nfcTagId: null,
            status: domain.HubStatus.active,
          );
    });
  }

  @override
  Future<void> deleteHub(String id) async {
    return _service.run(() async {
      final String businessId = await _service.getBusinessId();

      final QueryResult<dc.ListOrdersByBusinessAndTeamHubData,
              dc.ListOrdersByBusinessAndTeamHubVariables> result =
          await _service.connector
              .listOrdersByBusinessAndTeamHub(
                businessId: businessId,
                teamHubId: id,
              )
              .execute();

      if (result.data.orders.isNotEmpty) {
        throw HubHasOrdersException(
          technicalMessage: 'Hub $id has ${result.data.orders.length} orders',
        );
      }

      await _service.connector.deleteTeamHub(id: id).execute();
    });
  }

  @override
  Future<void> assignNfcTag({
    required String hubId,
    required String nfcTagId,
  }) {
    throw UnimplementedError('NFC tag assignment is not supported for team hubs.');
  }

  Future<dc.GetBusinessesByUserIdBusinesses> _getBusinessForCurrentUser() async {
    final dc.ClientSession? session = dc.ClientSessionStore.instance.session;
    final dc.ClientBusinessSession? cachedBusiness = session?.business;
    if (cachedBusiness != null) {
      return dc.GetBusinessesByUserIdBusinesses(
        id: cachedBusiness.id,
        businessName: cachedBusiness.businessName,
        userId: _service.auth.currentUser?.uid ?? '',
        rateGroup: const dc.Known<dc.BusinessRateGroup>(dc.BusinessRateGroup.STANDARD),
        status: const dc.Known<dc.BusinessStatus>(dc.BusinessStatus.ACTIVE),
        contactName: cachedBusiness.contactName,
        companyLogoUrl: cachedBusiness.companyLogoUrl,
        phone: null,
        email: cachedBusiness.email,
        hubBuilding: null,
        address: null,
        city: cachedBusiness.city,
        area: null,
        sector: null,
        notes: null,
        createdAt: null,
        updatedAt: null,
      );
    }

    final firebase.User? user = _service.auth.currentUser;
    if (user == null) {
      throw const NotAuthenticatedException(
        technicalMessage: 'No Firebase user in currentUser',
      );
    }

    final QueryResult<dc.GetBusinessesByUserIdData,
            dc.GetBusinessesByUserIdVariables> result =
        await _service.connector.getBusinessesByUserId(
              userId: user.uid,
            ).execute();
    if (result.data.businesses.isEmpty) {
      await _service.auth.signOut();
      throw BusinessNotFoundException(
        technicalMessage: 'No business found for user ${user.uid}',
      );
    }

    final dc.GetBusinessesByUserIdBusinesses business = result.data.businesses.first;
    if (session != null) {
      dc.ClientSessionStore.instance.setSession(
        dc.ClientSession(
          user: session.user,
          userPhotoUrl: session.userPhotoUrl,
          business: dc.ClientBusinessSession(
            id: business.id,
            businessName: business.businessName,
            email: business.email,
            city: business.city,
            contactName: business.contactName,
            companyLogoUrl: business.companyLogoUrl,
          ),
        ),
      );
    }

    return business;
  }

  Future<String> _getOrCreateTeamId(
    dc.GetBusinessesByUserIdBusinesses business,
  ) async {
    final QueryResult<dc.GetTeamsByOwnerIdData, dc.GetTeamsByOwnerIdVariables>
        teamsResult = await _service.connector.getTeamsByOwnerId(
              ownerId: business.id,
            ).execute();
    if (teamsResult.data.teams.isNotEmpty) {
      return teamsResult.data.teams.first.id;
    }

    final dc.CreateTeamVariablesBuilder createTeamBuilder = _service.connector.createTeam(
      teamName: '${business.businessName} Team',
      ownerId: business.id,
      ownerName: business.contactName ?? '',
      ownerRole: 'OWNER',
    );
    if (business.email != null) {
      createTeamBuilder.email(business.email);
    }

    final OperationResult<dc.CreateTeamData, dc.CreateTeamVariables>
        createTeamResult =
        await createTeamBuilder.execute();
    final String teamId = createTeamResult.data.team_insert.id;

    return teamId;
  }

  Future<List<domain.Hub>> _fetchHubsForTeam({
    required String teamId,
    required String businessId,
  }) async {
    final QueryResult<dc.GetTeamHubsByTeamIdData,
            dc.GetTeamHubsByTeamIdVariables> hubsResult =
        await _service.connector.getTeamHubsByTeamId(
              teamId: teamId,
            ).execute();

    return hubsResult.data.teamHubs
        .map(
          (dc.GetTeamHubsByTeamIdTeamHubs hub) => domain.Hub(
            id: hub.id,
            businessId: businessId,
            name: hub.hubName,
            address: hub.address,
            nfcTagId: null,
            status:
                hub.isActive
                    ? domain.HubStatus.active
                    : domain.HubStatus.inactive,
          ),
        )
        .toList();
  }

  Future<_PlaceAddress?> _fetchPlaceAddress(String placeId) async {
    final Uri uri = Uri.https(
      'maps.googleapis.com',
      '/maps/api/place/details/json',
      <String, String>{
        'place_id': placeId,
        'fields': 'address_component',
        'key': AppConfig.googleMapsApiKey,
      },
    );
    try {
      final http.Response response = await http.get(uri);
      if (response.statusCode != 200) {
        return null;
      }

      final Map<String, dynamic> payload =
          json.decode(response.body) as Map<String, dynamic>;
      if (payload['status'] != 'OK') {
        return null;
      }

      final Map<String, dynamic>? result =
          payload['result'] as Map<String, dynamic>?;
      final List<dynamic>? components =
          result?['address_components'] as List<dynamic>?;
      if (components == null || components.isEmpty) {
        return null;
      }

      String? streetNumber;
      String? route;
      String? city;
      String? state;
      String? country;
      String? zipCode;

      for (final dynamic entry in components) {
        final Map<String, dynamic> component = entry as Map<String, dynamic>;
        final List<dynamic> types = component['types'] as List<dynamic>? ?? <dynamic>[];
        final String? longName = component['long_name'] as String?;
        final String? shortName = component['short_name'] as String?;

        if (types.contains('street_number')) {
          streetNumber = longName;
        } else if (types.contains('route')) {
          route = longName;
        } else if (types.contains('locality')) {
          city = longName;
        } else if (types.contains('postal_town')) {
          city ??= longName;
        } else if (types.contains('administrative_area_level_2')) {
          city ??= longName;
        } else if (types.contains('administrative_area_level_1')) {
          state = shortName ?? longName;
        } else if (types.contains('country')) {
          country = shortName ?? longName;
        } else if (types.contains('postal_code')) {
          zipCode = longName;
        }
      }

      final String streetValue = <String?>[streetNumber, route]
          .where((String? value) => value != null && value.isNotEmpty)
          .join(' ')
          .trim();

      return _PlaceAddress(
        street: streetValue.isEmpty == true ? null : streetValue,
        city: city,
        state: state,
        country: country,
        zipCode: zipCode,
      );
    } catch (_) {
      return null;
    }
  }
}

class _PlaceAddress {
  const _PlaceAddress({
    this.street,
    this.city,
    this.state,
    this.country,
    this.zipCode,
  });

  final String? street;
  final String? city;
  final String? state;
  final String? country;
  final String? zipCode;
}
