import { SupabaseClient } from '@supabase/supabase-js';

import type { UuringuVastus } from '@kit/shared/types/medipost-analysis';
import { toArray } from '@kit/shared/utils';
import { Database } from '@kit/supabase/database';

import type {
  AnalysisOrder,
  AnalysisOrderStatus,
} from '../types/analysis-orders';
import type {
  AnalysisResultDetailsElement,
  AnalysisResultDetailsMapped,
  AnalysisResultLevel,
  AnalysisResultsDetailsElementNested,
  AnalysisResultsQuery,
  AnalysisStatus,
  UserAnalysis,
} from '../types/analysis-results';

/**
 * Class representing an API for interacting with user accounts.
 * @constructor
 * @param {SupabaseClient<Database>} client - The Supabase client instance.
 */
class UserAnalysesApi {
  constructor(private readonly client: SupabaseClient<Database>) {}

  async getAnalysisOrder({
    medusaOrderId,
    analysisOrderId,
  }: {
    medusaOrderId?: string;
    analysisOrderId?: number;
  }) {
    const query = this.client
      .schema('medreport')
      .from('analysis_orders')
      .select('*');
    if (medusaOrderId) {
      query.eq('medusa_order_id', medusaOrderId);
    } else if (analysisOrderId) {
      query.eq('id', analysisOrderId);
    } else {
      throw new Error('Either medusaOrderId or orderId must be provided');
    }

    const { data: order, error } = await query.single();
    if (error) {
      throw new Error(
        `Failed to get order by medusaOrderId=${medusaOrderId} or analysisOrderId=${analysisOrderId}, message=${error.message}, data=${JSON.stringify(order)}`,
      );
    }
    return order as AnalysisOrder;
  }

  async getUserAnalysis(
    analysisOrderId: number,
  ): Promise<AnalysisResultDetailsMapped | null> {
    const analysisOrder = await this.getAnalysisOrder({ analysisOrderId });
    const orderedAnalysisElementIds = analysisOrder.analysis_element_ids ?? [];
    if (orderedAnalysisElementIds.length === 0) {
      console.error(
        'No ordered analysis element ids found for analysis order id=',
        analysisOrderId,
      );
      return null;
    }
    const orderedAnalysisElements = await this.getOrderedAnalysisElements({
      analysisOrderId,
      orderedAnalysisElementIds,
    });

    const orderedAnalysisElementOriginalIds = orderedAnalysisElements.map(
      ({ analysis_id_original }) => analysis_id_original,
    );
    if (orderedAnalysisElementOriginalIds.length === 0) {
      console.error(
        'No ordered analysis element original ids found for analysis order id=',
        analysisOrderId,
      );
      return null;
    }

    const responseWithElements = await this.getAnalysisResponseWithElements({
      analysisOrderId,
    });
    if (!responseWithElements) {
      return null;
    }

    const mappedOrderedAnalysisElements =
      await this.getMappedOrderedAnalysisElements({
        analysisResponseElements: responseWithElements.elements,
        orderedAnalysisElements,
      });

    const feedback =
      responseWithElements.summary?.doctor_analysis_feedback?.[0];
    return {
      id: analysisOrderId,
      order: {
        status: analysisOrder.status,
        medusaOrderId: analysisOrder.medusa_order_id,
        createdAt: analysisOrder.created_at,
      },
      orderedAnalysisElements: mappedOrderedAnalysisElements,
      summary:
        feedback?.status === 'COMPLETED'
          ? (responseWithElements.summary?.doctor_analysis_feedback?.[0] ??
            null)
          : null,
    };
  }

  async getAnalysisResponseWithElements({
    analysisOrderId,
  }: {
    analysisOrderId: number;
  }) {
    const { data, error: userError } = await this.client.auth.getUser();
    if (userError) {
      throw userError;
    }
    const { user } = data;

    const { data: analysisResponse } = await this.client
      .schema('medreport')
      .from('analysis_responses')
      .select(
        `*, 
        elements:analysis_response_elements(analysis_name,norm_status,response_value,unit,norm_lower_included,norm_upper_included,norm_lower,norm_upper,response_time,status,analysis_element_original_id,original_response_element,response_value_is_negative,response_value_is_within_norm),
        summary:analysis_order_id(doctor_analysis_feedback(*))`,
      )
      .eq('user_id', user.id)
      .eq('analysis_order_id', analysisOrderId)
      .throwOnError();

    return analysisResponse?.[0] as AnalysisResultsQuery | null;
  }

  async getOrderedAnalysisElements({
    analysisOrderId,
    orderedAnalysisElementIds,
  }: {
    analysisOrderId: number;
    orderedAnalysisElementIds: number[];
  }) {
    const {
      data: orderedAnalysisElements,
      error: orderedAnalysisElementsError,
    } = await this.client
      .schema('medreport')
      .from('analysis_elements')
      .select('analysis_id_original,analysis_name_lab')
      .in('id', orderedAnalysisElementIds);
    if (orderedAnalysisElementsError) {
      console.error(
        `Failed to get ordered analysis elements for analysis order id=${analysisOrderId}`,
        orderedAnalysisElementsError,
      );
      throw orderedAnalysisElementsError;
    }
    return orderedAnalysisElements;
  }

  async getMappedOrderedAnalysisElements({
    analysisResponseElements,
    orderedAnalysisElements,
  }: {
    analysisResponseElements: AnalysisResultsQuery['elements'];
    orderedAnalysisElements: {
      analysis_id_original: string;
      analysis_name_lab: string;
    }[];
  }): Promise<AnalysisResultDetailsElement[]> {
    const mappedOrderedAnalysisElements = orderedAnalysisElements
      .map(({ analysis_id_original, analysis_name_lab }) => {
        return this.getOrderedAnalysisElement({
          analysisIdOriginal: analysis_id_original,
          analysisNameLab: analysis_name_lab,
          analysisResponseElements,
        });
      })
      .sort((a, b) => a.analysisName.localeCompare(b.analysisName));

    const nestedAnalysisElementIds = mappedOrderedAnalysisElements
      .map(({ results }) =>
        results?.nestedElements.map(
          ({ analysisElementOriginalId }) => analysisElementOriginalId,
        ),
      )
      .flat()
      .filter(Boolean);
    if (nestedAnalysisElementIds.length > 0) {
      const {
        data: nestedAnalysisElements,
        error: nestedAnalysisElementsError,
      } = await this.client
        .schema('medreport')
        .from('analysis_elements')
        .select('*')
        .in('analysis_id_original', nestedAnalysisElementIds);
      if (!nestedAnalysisElementsError && nestedAnalysisElements) {
        for (const mappedOrderedAnalysisElement of mappedOrderedAnalysisElements) {
          const { results } = mappedOrderedAnalysisElement;
          if (!results) {
            continue;
          }
          for (const nestedElement of results.nestedElements) {
            const { analysisElementOriginalId } = nestedElement;
            const nestedAnalysisElement = nestedAnalysisElements.find(
              ({ analysis_id_original }) =>
                analysis_id_original === analysisElementOriginalId,
            );
            if (!nestedAnalysisElement) {
              continue;
            }

            nestedElement.analysisElementOriginalId = analysisElementOriginalId;
            nestedElement.analysisName =
              nestedAnalysisElement.analysis_name_lab as string | undefined;
          }
          results.nestedElements = results.nestedElements.sort(
            (a, b) => a.analysisName?.localeCompare(b.analysisName ?? '') ?? 0,
          );
        }
      } else {
        console.error(
          'Failed to get nested analysis elements by ids=',
          nestedAnalysisElementIds,
          nestedAnalysisElementsError,
        );
      }
    }

    return mappedOrderedAnalysisElements;
  }

  getOrderedAnalysisElement({
    analysisIdOriginal,
    analysisNameLab,
    analysisResponseElements,
  }: {
    analysisIdOriginal: string;
    analysisNameLab: string;
    analysisResponseElements: AnalysisResultsQuery['elements'];
  }): AnalysisResultDetailsElement {
    const elementResponse = analysisResponseElements.find(
      (element) => element.analysis_element_original_id === analysisIdOriginal,
    );
    if (!elementResponse) {
      return {
        analysisIdOriginal,
        isWaitingForResults: true,
        analysisName: analysisNameLab,
      };
    }
    const labComment =
      elementResponse.original_response_element?.UuringuKommentaar;
    return {
      analysisIdOriginal,
      isWaitingForResults: false,
      analysisName: analysisNameLab,
      results: {
        nestedElements: ((): AnalysisResultsDetailsElementNested[] => {
          const nestedElements = toArray(
            elementResponse.original_response_element?.UuringuElement,
          );
          return nestedElements.map<AnalysisResultsDetailsElementNested>(
            (element) => {
              const mappedResponse = this.mapUuringVastus({
                uuringVastus: element.UuringuVastus as
                  | UuringuVastus
                  | undefined,
              });
              return {
                unit: element.Mootyhik ?? null,
                normLower: mappedResponse.normLower,
                normUpper: mappedResponse.normUpper,
                normStatus: mappedResponse.normStatus,
                responseTime: mappedResponse.responseTime,
                responseValue: mappedResponse.responseValue,
                responseValueIsNegative: mappedResponse.responseValueIsNegative,
                responseValueIsWithinNorm:
                  mappedResponse.responseValueIsWithinNorm,
                normLowerIncluded: mappedResponse.normLowerIncluded,
                normUpperIncluded: mappedResponse.normUpperIncluded,
                analysisElementOriginalId: element.UuringId,
                status: Number(elementResponse.status) as AnalysisStatus,
                analysisName: undefined,
              };
            },
          );
        })(),
        labComment,
        unit: elementResponse.unit,
        normLower: elementResponse.norm_lower,
        normUpper: elementResponse.norm_upper,
        normStatus: elementResponse.norm_status,
        responseTime: elementResponse.response_time,
        responseValue: elementResponse.response_value,
        responseValueIsNegative:
          elementResponse.response_value_is_negative === null
            ? null
            : elementResponse.response_value_is_negative === true,
        responseValueIsWithinNorm:
          elementResponse.response_value_is_within_norm === null
            ? null
            : elementResponse.response_value_is_within_norm === true,
        normLowerIncluded: elementResponse.norm_lower_included,
        normUpperIncluded: elementResponse.norm_upper_included,
        status: Number(elementResponse.status) as AnalysisStatus,
        analysisElementOriginalId: elementResponse.analysis_element_original_id,
      },
    };
  }

  mapUuringVastus({ uuringVastus }: { uuringVastus?: UuringuVastus }) {
    const vastuseVaartus = uuringVastus?.VastuseVaartus;
    const responseValue = (() => {
      const valueAsNumber = Number(vastuseVaartus);
      if (isNaN(valueAsNumber)) {
        return null;
      }
      return valueAsNumber;
    })();
    const responseValueNumber = Number(responseValue);
    const responseValueIsNumeric = !isNaN(responseValueNumber);
    const responseValueIsNegative = vastuseVaartus === 'Negatiivne';
    const responseValueIsWithinNorm = vastuseVaartus === 'Normi piires';
    return {
      normLower: uuringVastus?.NormAlum?.['#text'] ?? null,
      normUpper: uuringVastus?.NormYlem?.['#text'] ?? null,
      normStatus: (uuringVastus?.NormiStaatus ??
        null) as AnalysisResultLevel | null,
      responseTime: uuringVastus?.VastuseAeg ?? null,
      responseValue:
        responseValueIsNegative || !responseValueIsNumeric
          ? null
          : (responseValueNumber ?? null),
      responseValueIsNegative: responseValueIsNumeric
        ? null
        : responseValueIsNegative,
      responseValueIsWithinNorm: responseValueIsNumeric
        ? null
        : responseValueIsWithinNorm,
      normLowerIncluded:
        uuringVastus?.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah',
      normUpperIncluded:
        uuringVastus?.NormYlem?.['@_kaasaarvatud'].toLowerCase() === 'jah',
    };
  }

  // @TODO unused currently
  async getUserAnalyses(): Promise<UserAnalysis | null> {
    const authUser = await this.client.auth.getUser();
    const { data, error: userError } = authUser;

    if (userError) {
      console.error('Failed to get user', userError);
      throw userError;
    }

    const { user } = data;

    const { data: analysisResponses } = await this.client
      .schema('medreport')
      .from('analysis_responses')
      .select('*')
      .eq('user_id', user.id);

    if (!analysisResponses) {
      return null;
    }

    const analysisResponseIds = analysisResponses.map((r) => r.id);

    const { data: analysisResponseElements } = await this.client
      .schema('medreport')
      .from('analysis_response_elements')
      .select('*')
      .in('analysis_response_id', analysisResponseIds);

    if (!analysisResponseElements) {
      return null;
    }

    return analysisResponses.map((r) => ({
      ...r,
      elements: analysisResponseElements.filter(
        (e) => e.analysis_response_id === r.id,
      ),
    }));
  }

  async hasAccountTeamMembership(accountId?: string) {
    if (!accountId) {
      return false;
    }

    const { count, error } = await this.client
      .schema('medreport')
      .from('accounts_memberships')
      .select('account_id', { count: 'exact', head: true })
      .eq('account_id', accountId);

    if (error) {
      throw error;
    }

    return (count ?? 0) > 0;
  }

  async fetchBmiThresholds() {
    // Fetch BMI
    const { data, error } = await this.client
      .schema('medreport')
      .from('bmi_thresholds')
      .select(
        'age_min,age_max,underweight_max,normal_min,normal_max,overweight_min,strong_min,obesity_min',
      )
      .order('age_min', { ascending: true });

    if (error) {
      console.error('Error fetching BMI thresholds:', error);
      throw error;
    }
    return data;
  }

  async getAllUserAnalysisResponses(): Promise<
    Database['medreport']['Functions']['get_latest_analysis_response_elements_for_current_user']['Returns']
  > {
    const {
      data: { user },
    } = await this.client.auth.getUser();

    if (!user) {
      return [];
    }

    const { data, error } = await this.client
      .schema('medreport')
      .rpc('get_latest_analysis_response_elements_for_current_user', {
        p_user_id: user.id,
      });

    if (error) {
      console.error('Error fetching user analysis responses: ', error);
      throw error;
    }

    return data;
  }

  async updateAnalysisOrderStatus({
    orderId,
    medusaOrderId,
    orderStatus,
  }: {
    orderId?: number;
    medusaOrderId?: string;
    orderStatus: AnalysisOrderStatus;
  }) {
    const orderIdParam = orderId;
    const medusaOrderIdParam = medusaOrderId;

    console.info(
      `Updating order id=${orderId} medusaOrderId=${medusaOrderId} status=${orderStatus}`,
    );
    if (!orderIdParam && !medusaOrderIdParam) {
      throw new Error('Either orderId or medusaOrderId must be provided');
    }
    await this.client
      .schema('medreport')
      .rpc('update_analysis_order_status', {
        order_id: orderIdParam ?? -1,
        status_param: orderStatus,
        medusa_order_id_param: medusaOrderIdParam ?? '',
      })
      .throwOnError();
  }
}

export function createUserAnalysesApi(client: SupabaseClient<Database>) {
  return new UserAnalysesApi(client);
}
