'use server';

import type { PostgrestError } from '@supabase/supabase-js';

import { GetMessageListResponse, MedipostAction } from '@/lib/types/medipost';
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis';
import type {
  MedipostOrderResponse,
  ResponseUuringuGrupp,
  UuringElement,
} from '@/packages/shared/src/types/medipost-analysis';
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
import axios from 'axios';

import { toArray } from '@kit/shared/utils';
import { Tables } from '@kit/supabase/database';

import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
import type { AnalysisOrder } from '~/lib/types/order';

import { getAccountAdmin } from '../account.service';
import { getAnalyses } from '../analyses.service';
import { getAnalysisElementsAdmin } from '../analysis-element.service';
import {
  getExistingAnalysisResponseElements,
  upsertAnalysisResponse,
  upsertAnalysisResponseElement,
} from '../analysis-order.service';
import { logMedipostDispatch } from '../audit.service';
import { getAnalysisOrder } from '../order.service';
import { parseXML } from '../util/xml.service';
import { MedipostValidationError } from './MedipostValidationError';
import {
  getLatestMessage,
  upsertMedipostActionLog,
} from './medipostMessageBase.service';
import { validateMedipostResponse } from './medipostValidate.service';
import { OrderedAnalysisElement, composeOrderXML } from './medipostXML.service';

const BASE_URL = process.env.MEDIPOST_URL!;
const USER = process.env.MEDIPOST_USER!;
const PASSWORD = process.env.MEDIPOST_PASSWORD!;
const RECIPIENT = process.env.MEDIPOST_RECIPIENT!;

const IS_ENABLED_DELETE_PRIVATE_MESSAGE =
  process.env.MEDIPOST_ENABLE_DELETE_RESPONSE_PRIVATE_MESSAGE_ON_READ ===
  'true';

export async function getLatestPrivateMessageListItem({
  excludedMessageIds,
}: {
  excludedMessageIds: string[];
}) {
  const { data } = await axios.get<GetMessageListResponse>(BASE_URL, {
    params: {
      Action: MedipostAction.GetPrivateMessageList,
      User: USER,
      Password: PASSWORD,
    },
  });

  if (data.code && data.code !== 0) {
    throw new Error('Failed to get private message list');
  }

  return await getLatestMessage({
    messages: data?.messages,
    excludedMessageIds,
  });
}

const logger =
  (
    analysisOrder: AnalysisOrder,
    externalId: string,
    analysisResponseId: string,
  ) =>
  (message: string, error?: PostgrestError | null) => {
    const messageFormatted = `[${analysisOrder.id}] [${externalId}] [${analysisResponseId}] ${message}`;
    if (error) {
      console.info(messageFormatted, error);
    } else {
      console.info(messageFormatted);
    }
  };

export async function canCreateAnalysisResponseElement({
  existingElements,
  groupUuring: {
    UuringuElement: { UuringOlek: status, UuringId: analysisElementOriginalId },
  },
  responseValue,
  log,
}: {
  existingElements: Pick<
    AnalysisResponseElement,
    'analysis_element_original_id' | 'status' | 'response_value'
  >[];
  groupUuring: {
    UuringuElement: Pick<UuringElement, 'UuringOlek' | 'UuringId'>;
  };
  responseValue: number | null;
  log: ReturnType<typeof logger>;
}) {
  const existingAnalysisResponseElement = existingElements.find(
    ({ analysis_element_original_id }) =>
      analysis_element_original_id === analysisElementOriginalId,
  );
  if (!existingAnalysisResponseElement) {
    return true;
  }

  if (Number(existingAnalysisResponseElement.status) > status) {
    log(
      `Analysis response element id=${analysisElementOriginalId} already exists for order in higher status ${existingAnalysisResponseElement.status} than ${status}`,
    );
    return false;
  }

  if (existingAnalysisResponseElement.response_value && !responseValue) {
    log(
      `Analysis response element id=${analysisElementOriginalId} already exists for order with response value ${existingAnalysisResponseElement.response_value} but new response has no value`,
    );
    return false;
  }

  return true;
}

export async function getAnalysisResponseElementsForGroup({
  analysisGroup,
  existingElements,
  log,
}: {
  analysisGroup: Pick<ResponseUuringuGrupp, 'UuringuGruppNimi' | 'Uuring'>;
  existingElements: Pick<
    AnalysisResponseElement,
    'analysis_element_original_id' | 'status' | 'response_value'
  >[];
  log: ReturnType<typeof logger>;
}) {
  const groupUuringItems = toArray(
    analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'],
  );
  log(
    `Order has results in group '${analysisGroup.UuringuGruppNimi}' for ${groupUuringItems.length} analysis elements`,
  );

  const results: Omit<
    AnalysisResponseElement,
    'created_at' | 'updated_at' | 'id' | 'analysis_response_id'
  >[] = [];

  for (const groupUuring of groupUuringItems) {
    const groupUuringElement = groupUuring.UuringuElement;
    const elementAnalysisResponses = toArray(groupUuringElement.UuringuVastus);

    const status = groupUuringElement.UuringOlek;
    log(
      `Group uuring '${analysisGroup.UuringuGruppNimi}' has status ${status}`,
    );

    for (const response of elementAnalysisResponses) {
      const analysisElementOriginalId = groupUuringElement.UuringId;
      const vastuseVaartus = response.VastuseVaartus;
      const responseValue = (() => {
        const valueAsNumber = Number(vastuseVaartus);
        if (isNaN(valueAsNumber)) {
          return null;
        }
        return valueAsNumber;
      })();

      if (
        !(await canCreateAnalysisResponseElement({
          existingElements,
          groupUuring,
          responseValue,
          log,
        }))
      ) {
        continue;
      }

      const mappedResponse = createUserAnalysesApi(
        getSupabaseServerAdminClient(),
      ).mapUuringVastus({ uuringVastus: response });

      results.push({
        analysis_element_original_id: analysisElementOriginalId,
        norm_lower: mappedResponse.normLower,
        norm_lower_included: mappedResponse.normLowerIncluded,
        norm_status: mappedResponse.normStatus,
        norm_upper: mappedResponse.normUpper,
        norm_upper_included: mappedResponse.normUpperIncluded,
        response_time: mappedResponse.responseTime,
        response_value: mappedResponse.responseValue,
        unit: groupUuringElement.Mootyhik ?? null,
        original_response_element: groupUuringElement,
        analysis_name:
          groupUuringElement.UuringNimi || groupUuringElement.KNimetus,
        comment: groupUuringElement.UuringuKommentaar ?? null,
        status: status.toString(),
        response_value_is_within_norm: mappedResponse.responseValueIsWithinNorm,
        response_value_is_negative: mappedResponse.responseValueIsNegative,
      });
    }
  }

  return results;
}

async function getNewAnalysisResponseElements({
  analysisGroups,
  existingElements,
  log,
}: {
  analysisGroups: ResponseUuringuGrupp[];
  existingElements: AnalysisResponseElement[];
  log: ReturnType<typeof logger>;
}) {
  const newElements: Omit<
    AnalysisResponseElement,
    'created_at' | 'updated_at' | 'id' | 'analysis_response_id'
  >[] = [];
  for (const analysisGroup of analysisGroups) {
    log(
      `[${analysisGroups.indexOf(analysisGroup) + 1}/${analysisGroups.length}] Syncing analysis group '${analysisGroup.UuringuGruppNimi}'`,
    );
    const elements = await getAnalysisResponseElementsForGroup({
      analysisGroup,
      existingElements,
      log,
    });
    newElements.push(...elements);
  }
  return newElements;
}

async function hasAllAnalysisResponseElements({
  analysisResponseId,
  order,
}: {
  analysisResponseId: number;
  order: Pick<AnalysisOrder, 'analysis_element_ids'>;
}) {
  const allOrderResponseElements = await getExistingAnalysisResponseElements({
    analysisResponseId,
  });
  const expectedOrderResponseElements = order.analysis_element_ids?.length ?? 0;
  return allOrderResponseElements.length >= expectedOrderResponseElements;
}

export async function syncPrivateMessage({
  messageResponse: {
    ValisTellimuseId: externalId,
    TellimuseNumber: orderNumber,
    TellimuseOlek,
    UuringuGrupp,
  },
  order,
}: {
  messageResponse: Pick<
    NonNullable<MedipostOrderResponse['Saadetis']['Vastus']>,
    'ValisTellimuseId' | 'TellimuseNumber' | 'TellimuseOlek' | 'UuringuGrupp'
  >;
  order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
}) {
  const supabase = getSupabaseServerAdminClient();

  const orderStatus = AnalysisOrderStatus[TellimuseOlek];

  const log = logger(order, externalId, orderNumber);

  const { data: analysisOrder } = await supabase
    .schema('medreport')
    .from('analysis_orders')
    .select('id, user_id')
    .eq('id', order.id)
    .single()
    .throwOnError();

  const { analysisResponseId } = await upsertAnalysisResponse({
    analysisOrderId: order.id,
    orderNumber,
    orderStatus,
    userId: analysisOrder.user_id,
  });

  const existingElements = await getExistingAnalysisResponseElements({
    analysisResponseId,
  });

  const analysisGroups = toArray(UuringuGrupp);
  log(`Order has results for ${analysisGroups.length} analysis groups`);
  const newElements = await getNewAnalysisResponseElements({
    analysisGroups,
    existingElements,
    log,
  });

  for (const element of newElements) {
    try {
      await upsertAnalysisResponseElement({
        element: {
          ...element,
          analysis_response_id: analysisResponseId,
        },
      });
    } catch (e) {
      log(
        `Failed to create order response element for response id ${analysisResponseId}, element id '${element.analysis_element_original_id}' (order id: ${order.id})`,
        e as PostgrestError,
      );
    }
  }

  return (await hasAllAnalysisResponseElements({ analysisResponseId, order }))
    ? { isCompleted: orderStatus === 'COMPLETED' }
    : { isPartial: true };
}

export async function readPrivateMessageResponse({
  excludedMessageIds,
}: {
  excludedMessageIds: string[];
}): Promise<{
  messageId: string | null;
  hasAnalysisResponse: boolean;
  hasPartialAnalysisResponse: boolean;
  hasFullAnalysisResponse: boolean;
  medusaOrderId: string | undefined;
  analysisOrderId: number | undefined;
}> {
  let messageId: string | null = null;
  let hasAnalysisResponse = false;
  let hasPartialAnalysisResponse = false;
  let hasFullAnalysisResponse = false;
  let medusaOrderId: string | undefined = undefined;
  let analysisOrderId: number | undefined = undefined;

  try {
    const privateMessage = await getLatestPrivateMessageListItem({
      excludedMessageIds,
    });
    messageId = privateMessage?.messageId ?? null;

    if (!privateMessage || !messageId) {
      return {
        messageId: null,
        hasAnalysisResponse: false,
        hasPartialAnalysisResponse: false,
        hasFullAnalysisResponse: false,
        medusaOrderId: undefined,
        analysisOrderId: undefined,
      };
    }

    const { messageId: privateMessageId } = privateMessage;
    const { message: privateMessageContent, xml: privateMessageXml } =
      await getPrivateMessage(privateMessageId);

    const messageResponse = privateMessageContent?.Saadetis?.Vastus;
    const medipostExternalOrderId =
      privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId ||
      messageResponse?.ValisTellimuseId;
    const patientPersonalCode = messageResponse?.Patsient.Isikukood?.toString();
    analysisOrderId = Number(medipostExternalOrderId);

    const hasInvalidOrderId = isNaN(analysisOrderId);

    if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) {
      console.error(
        `Invalid order id or message response or patient personal code, medipostExternalOrderId=${medipostExternalOrderId}, privateMessageId=${privateMessageId}`,
      );
      await upsertMedipostActionLog({
        action: 'sync_analysis_results_from_medipost',
        xml: privateMessageXml,
        hasAnalysisResults: false,
        medipostPrivateMessageId: privateMessageId,
        medusaOrderId,
        medipostExternalOrderId,
        hasError: true,
      });
      return {
        messageId,
        hasAnalysisResponse: false,
        hasPartialAnalysisResponse: false,
        hasFullAnalysisResponse: false,
        medusaOrderId: hasInvalidOrderId ? undefined : medusaOrderId,
        analysisOrderId: hasInvalidOrderId ? undefined : analysisOrderId,
      };
    }

    let analysisOrder: AnalysisOrder;
    try {
      analysisOrder = await getAnalysisOrder({ analysisOrderId });
      medusaOrderId = analysisOrder.medusa_order_id;
    } catch (e) {
      if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) {
        await deletePrivateMessage(privateMessageId);
      }
      throw new Error(
        `No analysis order found for Medipost message ValisTellimuseId=${medipostExternalOrderId}`,
      );
    }

    const orderPerson = await getAccountAdmin({
      primaryOwnerUserId: analysisOrder.user_id,
    });
    if (orderPerson.personal_code !== patientPersonalCode) {
      throw new Error(
        `Order person personal code does not match Medipost message Patsient.Isikukood=${patientPersonalCode}, orderPerson.personal_code=${orderPerson.personal_code}`,
      );
    }

    const status = await syncPrivateMessage({
      messageResponse,
      order: analysisOrder,
    });

    console.info(
      `Successfully synced analysis results from Medipost message privateMessageId=${privateMessageId}`,
    );
    await upsertMedipostActionLog({
      action: 'sync_analysis_results_from_medipost',
      xml: privateMessageXml,
      hasAnalysisResults: true,
      medipostPrivateMessageId: privateMessageId,
      medusaOrderId,
      medipostExternalOrderId,
    });
    if (status.isPartial) {
      await createUserAnalysesApi(
        getSupabaseServerAdminClient(),
      ).updateAnalysisOrderStatus({
        medusaOrderId,
        orderStatus: 'PARTIAL_ANALYSIS_RESPONSE',
      });
      hasAnalysisResponse = true;
      hasPartialAnalysisResponse = true;
    } else if (status.isCompleted) {
      await createUserAnalysesApi(
        getSupabaseServerAdminClient(),
      ).updateAnalysisOrderStatus({
        medusaOrderId,
        orderStatus: 'FULL_ANALYSIS_RESPONSE',
      });
      if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) {
        await deletePrivateMessage(privateMessageId);
      }
      hasAnalysisResponse = true;
      hasFullAnalysisResponse = true;
    }
  } catch (e) {
    console.warn(
      `Failed to process private message id=${messageId}, message=${(e as Error).message}`,
    );
  }

  return {
    messageId,
    hasAnalysisResponse,
    hasPartialAnalysisResponse,
    hasFullAnalysisResponse,
    medusaOrderId,
    analysisOrderId,
  };
}

export async function deletePrivateMessage(messageId: string) {
  const { data } = await axios.get(BASE_URL, {
    params: {
      Action: MedipostAction.DeletePrivateMessage,
      User: USER,
      Password: PASSWORD,
      MessageId: messageId,
    },
  });

  if (data.code && data.code !== 0) {
    throw new Error(`Failed to delete private message (id: ${messageId})`);
  }
}

export async function sendPrivateMessage(messageXml: string) {
  const body = new FormData();
  body.append('Action', MedipostAction.SendPrivateMessage);
  body.append('User', USER);
  body.append('Password', PASSWORD);
  body.append('Receiver', RECIPIENT);
  body.append('MessageType', 'Tellimus');
  body.append(
    'Message',
    new Blob([messageXml], {
      type: 'text/xml; charset=UTF-8',
    }),
  );

  const { data } = await axios.post(BASE_URL, body);

  await validateMedipostResponse(data);
}

export async function getPrivateMessage(messageId: string) {
  const { data } = await axios.get(BASE_URL, {
    params: {
      Action: MedipostAction.GetPrivateMessage,
      User: USER,
      Password: PASSWORD,
      MessageId: messageId,
    },
    headers: {
      Accept: 'application/xml',
    },
  });

  await validateMedipostResponse(data, { canHaveEmptyCode: true });

  return {
    message: (await parseXML(data)) as MedipostOrderResponse,
    xml: data as string,
  };
}

export async function sendOrderToMedipost({
  medusaOrderId,
  orderedAnalysisElements,
}: {
  medusaOrderId: string;
  orderedAnalysisElements: OrderedAnalysisElement[];
}) {
  const medreportOrder = await getAnalysisOrder({ medusaOrderId });
  const account = await getAccountAdmin({
    primaryOwnerUserId: medreportOrder.user_id,
  });

  const orderedAnalysesIds = orderedAnalysisElements
    .map(({ analysisId }) => analysisId)
    .filter(Boolean) as number[];
  const orderedAnalysisElementsIds = orderedAnalysisElements
    .map(({ analysisElementId }) => analysisElementId)
    .filter(Boolean) as number[];

  const analyses = await getAnalyses({ ids: orderedAnalysesIds });
  if (analyses.length !== orderedAnalysesIds.length) {
    throw new Error(
      `Got ${analyses.length} analyses, expected ${orderedAnalysesIds.length}`,
    );
  }
  const analysisElements = await getAnalysisElementsAdmin({
    ids: orderedAnalysisElementsIds,
  });
  if (analysisElements.length !== orderedAnalysisElementsIds.length) {
    throw new Error(
      `Got ${analysisElements.length} analysis elements, expected ${orderedAnalysisElementsIds.length}`,
    );
  }

  const orderXml = await composeOrderXML({
    analyses,
    analysisElements,
    person: {
      idCode: account.personal_code!,
      firstName: account.name ?? '',
      lastName: account.last_name ?? '',
      phone: account.phone ?? '',
    },
    orderId: medreportOrder.id,
    orderCreatedAt: new Date(medreportOrder.created_at),
    comment: '',
  });

  try {
    await sendPrivateMessage(orderXml);
  } catch (e) {
    const isMedipostError = e instanceof MedipostValidationError;
    if (isMedipostError) {
      await logMedipostDispatch({
        medusaOrderId,
        isSuccess: false,
        isMedipostError,
        errorMessage: e.response,
      });
      console.error(
        `Failed to send order to Medipost, medusaOrderId=${medusaOrderId}, error=${e.response}`,
      );
      await upsertMedipostActionLog({
        action: 'send_order_to_medipost',
        xml: orderXml,
        hasAnalysisResults: false,
        medusaOrderId,
        responseXml: e.response,
        hasError: true,
      });
    } else {
      console.error(
        `Failed to send order to Medipost, medusaOrderId=${medusaOrderId}, error=${e}`,
      );
      await logMedipostDispatch({
        medusaOrderId,
        isSuccess: false,
        isMedipostError,
      });
      await upsertMedipostActionLog({
        action: 'send_order_to_medipost',
        xml: orderXml,
        hasAnalysisResults: false,
        medusaOrderId,
        hasError: true,
      });
    }

    throw e;
  }
  console.info(
    `Successfully sent order to Medipost, medusaOrderId=${medusaOrderId}`,
  );
  await logMedipostDispatch({
    medusaOrderId,
    isSuccess: true,
    isMedipostError: false,
  });
  await upsertMedipostActionLog({
    action: 'send_order_to_medipost',
    xml: orderXml,
    hasAnalysisResults: false,
    medusaOrderId,
  });
  await createUserAnalysesApi(
    getSupabaseServerAdminClient(),
  ).updateAnalysisOrderStatus({
    medusaOrderId,
    orderStatus: 'PROCESSING',
  });
}
