'use server';

import logRequestResult from '@/lib/services/audit.service';
import { RequestStatus } from '@/lib/types/audit';
import {
  AvailableAppointmentsResponse,
  BookTimeResponse,
  ConfirmedLoadResponse,
  ConnectedOnlineMethodName,
  FailureReason,
} from '@/lib/types/connected-online';
import { ExternalApi } from '@/lib/types/external';
import { Tables } from '@/packages/supabase/src/database.types';
import axios from 'axios';
import { uniq, uniqBy } from 'lodash';

import { renderBookTimeFailedEmail } from '@kit/email-templates';
import { getLogger } from '@kit/shared/logger';
import { getSupabaseServerClient } from '@kit/supabase/server-client';

import { TimeSlotResponse } from '~/home/(user)/_components/booking/booking.context';

import { sendEmailFromTemplate } from './mailer.service';

export async function getAvailableAppointmentsForService(
  serviceId: number,
  key: string,
  locationId: number | null,
  startTime?: Date,
  maxDays?: number,
) {
  try {
    const start = startTime ? { StartTime: startTime } : {};
    const response = await axios.post(
      `${process.env.CONNECTED_ONLINE_URL!}/${ConnectedOnlineMethodName.GetAvailabilities}`,
      {
        headers: {
          'Content-Type': 'application/json; charset=utf-8',
        },
        param: JSON.stringify({
          ServiceID: serviceId,
          Key: key,
          Lang: 'et',
          MaxDays: maxDays ?? 120,
          LocationId: locationId ?? -1,
          ...start,
        }),
      },
    );

    const responseData: AvailableAppointmentsResponse = JSON.parse(
      response.data.d,
    );

    if (
      responseData?.ErrorCode !== 0 ||
      !responseData.Data.T_Service?.length ||
      !responseData.Data.T_Booking?.length
    ) {
      let comment = `Response returned error code ${responseData.ErrorCode}, message: ${responseData.ErrorMessage}`;
      if (responseData?.ErrorCode === 0) {
        comment = responseData.Data.T_Service?.length
          ? `No service present in appointment availability response, service id: ${serviceId}, start time: ${startTime}`
          : `No booking times present in appointment availability response, service id: ${serviceId}, start time: ${startTime}`;
      }

      await logRequestResult(
        ExternalApi.ConnectedOnline,
        ConnectedOnlineMethodName.GetAvailabilities,
        RequestStatus.Fail,
        comment,
      );

      return null;
    }

    await logRequestResult(
      ExternalApi.ConnectedOnline,
      ConnectedOnlineMethodName.GetAvailabilities,
      RequestStatus.Success,
      JSON.stringify(responseData),
    );
    return responseData.Data;
  } catch (error) {
    await logRequestResult(
      ExternalApi.ConnectedOnline,
      ConnectedOnlineMethodName.GetAvailabilities,
      RequestStatus.Fail,
      JSON.stringify(error),
    );
    return null;
  }
}

export async function bookAppointment(
  serviceId: number,
  clinicId: number,
  appointmentUserId: number,
  syncUserID: number,
  startTime: string,
  comments = '',
): Promise<{
  success: boolean;
  reason?: FailureReason;
  bookingCode: string | null;
  clinicKey: string | null;
}> {
  const logger = await getLogger();
  const supabase = getSupabaseServerClient();

  let reason = FailureReason.BOOKING_FAILED;
  try {
    const {
      data: { user },
    } = await supabase.auth.getUser();

    logger.info(
      `Booking time slot ${JSON.stringify({ serviceId, clinicId, startTime, userId: user?.id })}`,
    );

    if (!user?.id) {
      throw new Error('User not authenticated');
    }

    const formattedStartTime = startTime.replace('T', ' ');

    const [
      { data: dbClinic, error: clinicError },
      { data: dbService, error: serviceError },
      { data: account, error: accountError },
      { data: dbReservation, error: dbReservationError },
    ] = await Promise.all([
      supabase
        .schema('medreport')
        .from('connected_online_providers')
        .select('*')
        .eq('id', clinicId)
        .single(),
      supabase
        .schema('medreport')
        .from('connected_online_services')
        .select('*')
        .eq('id', serviceId)
        .eq('clinic_id', clinicId)
        .single(),
      supabase
        .schema('medreport')
        .from('accounts')
        .select('name, last_name, personal_code, phone, email')
        .eq('is_personal_account', true)
        .eq('primary_owner_user_id', user.id)
        .single(),
      supabase
        .schema('medreport')
        .from('connected_online_reservation')
        .select('id,location_sync_id')
        .eq('clinic_id', clinicId)
        .eq('service_id', serviceId)
        .eq('start_time', formattedStartTime)
        .eq('user_id', user.id)
        .eq('status', 'PENDING')
        .single(),
    ]);

    if (!dbClinic || !dbService) {
      const errorMessage = dbClinic
        ? `Could not find clinic with id ${clinicId}, error: ${JSON.stringify(clinicError)}`
        : `Could not find service with sync id ${serviceId} and clinic id ${clinicId}, error: ${JSON.stringify(serviceError)}`;
      logger.error(errorMessage);

      throw new Error(errorMessage);
    }

    if (clinicError || serviceError || accountError) {
      const stringifiedErrors = JSON.stringify({
        clinicError,
        serviceError,
        accountError,
      });
      const errorMessage = `Failed to book time, error: ${stringifiedErrors}`;
      logger.error(errorMessage);

      throw new Error(errorMessage);
    }

    if (!dbReservation) {
      const errorMessage = `No reservation found in db with data ${JSON.stringify({ clinicId, serviceId, startTime, userId: user.id })}, got error ${JSON.stringify(dbReservationError)}`;
      logger.error(errorMessage);

      throw new Error(errorMessage);
    }

    const clinic: Tables<
      { schema: 'medreport' },
      'connected_online_providers'
    > = dbClinic;
    const service: Tables<
      { schema: 'medreport' },
      'connected_online_services'
    > = dbService;

    const connectedOnlineBookingResponse = await axios.post(
      `${process.env.CONNECTED_ONLINE_URL!}/${ConnectedOnlineMethodName.BookTime}`,
      {
        headers: {
          'Content-Type': 'application/json; charset=utf-8',
        },
        param: JSON.stringify({
          ClinicID: clinic.id,
          ServiceID: service.id,
          ClinicServiceID: service.sync_id,
          UserID: appointmentUserId,
          SyncUserID: syncUserID,
          StartTime: startTime,
          FirstName: account.name,
          LastName: account.last_name,
          PersonalCode: account.personal_code,
          Email: account.email ?? user.email,
          Phone: account.phone,
          Comments: comments,
          AddToBasket: false,
          Key: dbClinic.key,
          Lang: 'et',
          Location: dbReservation.location_sync_id,
        }),
      },
    );

    const connectedOnlineBookingResponseData: BookTimeResponse = JSON.parse(
      connectedOnlineBookingResponse.data.d,
    );

    const errorCode = connectedOnlineBookingResponseData?.ErrorCode;
    if (errorCode !== 0 || !connectedOnlineBookingResponseData.Value) {
      const errorMessage = `Received invalid result from external api, error: ${JSON.stringify(connectedOnlineBookingResponseData)}`;
      logger.error(errorMessage);
      if (process.env.SUPPORT_EMAIL) {
        await sendEmailFromTemplate(
          renderBookTimeFailedEmail,
          { reservationId: dbReservation.id, error: errorMessage },
          process.env.SUPPORT_EMAIL,
        );
      }
      await supabase
        .schema('medreport')
        .from('connected_online_reservation')
        .update({
          status: 'REJECTED',
        })
        .eq('id', dbReservation.id)
        .throwOnError();

      if (errorCode === 1) {
        reason = FailureReason.TIME_SLOT_UNAVAILABLE;
      }

      throw new Error(errorMessage);
    }

    const responseParts = connectedOnlineBookingResponseData.Value.split(',');

    const { data: updatedReservation, error } = await supabase
      .schema('medreport')
      .from('connected_online_reservation')
      .update({
        booking_code: responseParts[1],
        requires_payment: !!responseParts[0],
        status: 'CONFIRMED',
      })
      .eq('id', dbReservation.id)
      .select('id')
      .single();

    if (error) {
      throw new Error(
        JSON.stringify({ connectedOnlineBookingResponseData, error }),
      );
    }

    logger.info(
      'Booked time, updated reservation with id ' + updatedReservation?.id,
    );

    if (responseParts[1]) {
      await logRequestResult(
        ExternalApi.ConnectedOnline,
        ConnectedOnlineMethodName.BookTime,
        RequestStatus.Success,
        JSON.stringify(connectedOnlineBookingResponseData),
        startTime.toString(),
        service.id,
        clinicId,
      );
      return {
        success: true,
        bookingCode: responseParts[1],
        clinicKey: dbClinic.key,
      };
    } else {
      logger.error(`Missing booking code: ${responseParts}`);
      await logRequestResult(
        ExternalApi.ConnectedOnline,
        ConnectedOnlineMethodName.BookTime,
        RequestStatus.Fail,
        JSON.stringify(error),
        startTime.toString(),
        serviceId,
        clinicId,
      );
      return { success: false, bookingCode: null, clinicKey: null };
    }
  } catch (error) {
    logger.error(`Failed to book time, error: ${JSON.stringify(error)}`);
    await logRequestResult(
      ExternalApi.ConnectedOnline,
      ConnectedOnlineMethodName.BookTime,
      RequestStatus.Fail,
      JSON.stringify(error),
      startTime.toString(),
      serviceId,
      clinicId,
    );
    return { success: false, reason, bookingCode: null, clinicKey: null };
  }
}

export async function getConfirmedService(
  reservationCode: string,
  clinicKey: string,
) {
  try {
    const url = new URL(process.env.CONNECTED_ONLINE_CONFIRMED_URL!);
    url.searchParams.set('code1', reservationCode);
    url.searchParams.set('key', clinicKey);
    url.searchParams.set('lang', 'et');
    await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
      },
    });

    await logRequestResult(
      ExternalApi.ConnectedOnline,
      ConnectedOnlineMethodName.ConfirmedLoad,
      RequestStatus.Success,
      'ok',
    );
    return;
  } catch (error) {
    await logRequestResult(
      ExternalApi.ConnectedOnline,
      ConnectedOnlineMethodName.ConfirmedLoad,
      RequestStatus.Fail,
      error?.toString(),
    );
    return null;
  }
}

export async function getAvailableTimeSlotsForDisplay(
  serviceIds: number[],
  locationId: number | null,
  date?: Date,
): Promise<TimeSlotResponse> {
  const supabase = getSupabaseServerClient();

  const { data: syncedServices } = await supabase
    .schema('medreport')
    .from('connected_online_services')
    .select(
      '*, providerClinic:clinic_id(*,locations:connected_online_locations(*))',
    )
    .in('id', serviceIds)
    .throwOnError();

  const timeSlotPromises = [];
  for (const syncedService of syncedServices) {
    const timeSlotsPromise = getAvailableAppointmentsForService(
      syncedService.id,
      syncedService.providerClinic.key,
      locationId,
      date,
    );
    timeSlotPromises.push(timeSlotsPromise);
  }

  const timeSlots = await Promise.all(timeSlotPromises);

  const mappedTimeSlots = [];
  for (const timeSlotGroup of timeSlots) {
    const { data: serviceProviders } = await supabase
      .schema('medreport')
      .from('connected_online_service_providers')
      .select(
        'name, id, jobTitleEn: job_title_en, jobTitleEt: job_title_et, jobTitleRu: job_title_ru, clinicId: clinic_id',
      )
      .in(
        'clinic_id',
        uniq(timeSlotGroup?.T_Booking.map(({ ClinicID }) => ClinicID)),
      )
      .throwOnError();

    const timeSlots =
      timeSlotGroup?.T_Booking?.map((item) => {
        return {
          ...item,
          serviceProvider: serviceProviders.find(
            ({ id }) => id === item.UserID,
          ),
          syncedService: syncedServices.find(
            (syncedService) => syncedService.sync_id === item.ServiceID,
          ),
          location: syncedServices
            .find(
              ({ providerClinic }) =>
                providerClinic.id === Number(item.ClinicID),
            )
            ?.providerClinic?.locations?.find(
              (location) => location.sync_id === item.LocationID,
            ),
        };
      }) ?? [];
    mappedTimeSlots.push(...timeSlots);
  }

  return {
    timeSlots: mappedTimeSlots,
    locations: uniqBy(
      syncedServices.flatMap(({ providerClinic }) => providerClinic.locations),
      'id',
    ),
  };
}
