'use server';

import {
  SupabaseClient,
  createClient as createCustomClient,
} from '@supabase/supabase-js';

import {
  getAnalysisGroup,
  getClientInstitution,
  getClientPerson,
  getConfidentiality,
  getOrderEnteredByPerson,
  getPais,
  getPatient,
  getProviderInstitution,
  getSpecimen,
} from '@/lib/templates/medipost-order';
import { SyncStatus } from '@/lib/types/audit';
import {
  AnalysisOrderStatus,
  GetMessageListResponse,
  MaterjalideGrupp,
  MedipostAction,
  MedipostOrderResponse,
  MedipostPublicMessageResponse,
  Message,
  ResponseUuringuGrupp,
  UuringuGrupp,
} from '@/lib/types/medipost';
import { toArray } from '@/lib/utils';
import { Tables } from '@/supabase/database.types';
import axios from 'axios';
import { XMLParser } from 'fast-xml-parser';
import { uniqBy } from 'lodash';

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

export async function getMessages() {
  try {
    const publicMessage = await getLatestPublicMessageListItem();

    if (!publicMessage) {
      return null;
    }

    //Teenused tuleb mappida kokku MedReport teenustega. <UuringId> alusel
    return getPublicMessage(publicMessage.messageId);
  } catch (error) {
    console.error(error);
  }
}

export async function getLatestPublicMessageListItem() {
  const { data } = await axios.get<GetMessageListResponse>(BASE_URL, {
    params: {
      Action: MedipostAction.GetPublicMessageList,
      User: USER,
      Password: PASSWORD,
      Sender: 'syndev',
      // LastChecked (date+time) can be used here to get only messages since the last check - add when cron is created
      // MessageType check only for messages of certain type
    },
  });

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

  return getLatestMessage(data?.messages);
}

export async function getPublicMessage(messageId: string) {
  const { data } = await axios.get(BASE_URL, {
    params: {
      Action: MedipostAction.GetPublicMessage,
      User: USER,
      Password: PASSWORD,
      MessageId: messageId,
    },
    headers: {
      Accept: 'application/xml',
    },
  });
  const parser = new XMLParser({ ignoreAttributes: false });
  const parsed: MedipostPublicMessageResponse = parser.parse(data);

  if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) {
    throw new Error(`Failed to get public message (id: ${messageId})`);
  }

  return parsed;
}

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

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

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

export async function getLatestPrivateMessageListItem() {
  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 getLatestMessage(data?.messages);
}

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',
    },
  });

  const parser = new XMLParser({ ignoreAttributes: false });
  const parsed = parser.parse(data);

  if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) {
    throw new Error(`Failed to get private message (id: ${messageId})`);
  }

  return parsed;
}

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 readPrivateMessageResponse() {
  try {
    const privateMessage = await getLatestPrivateMessageListItem();

    if (!privateMessage) {
      return null;
    }

    const privateMessageContent = await getPrivateMessage(
      privateMessage.messageId,
    );

    const status = await syncPrivateMessage(privateMessageContent);

    if (status === 'COMPLETED') {
      await deletePrivateMessage(privateMessage.messageId);
    }
  } catch (e) {
    console.error(e);
  }
}

async function saveAnalysisGroup(
  analysisGroup: UuringuGrupp,
  supabase: SupabaseClient,
) {
  const { data: insertedAnalysisGroup, error } = await supabase
    .from('analysis_groups')
    .upsert(
      {
        original_id: analysisGroup.UuringuGruppId,
        name: analysisGroup.UuringuGruppNimi,
        order: analysisGroup.UuringuGruppJarjekord,
      },
      { onConflict: 'original_id', ignoreDuplicates: false },
    )
    .select('id');

  if (error || !insertedAnalysisGroup[0]?.id) {
    throw new Error(
      `Failed to insert analysis group (id: ${analysisGroup.UuringuGruppId}), error: ${error?.message}`,
    );
  }
  const analysisGroupId = insertedAnalysisGroup[0].id;

  const analysisGroupCodes = toArray(analysisGroup.Kood);
  const codes: Partial<Tables<'codes'>>[] = analysisGroupCodes.map((kood) => ({
    hk_code: kood.HkKood,
    hk_code_multiplier: kood.HkKoodiKordaja,
    coefficient: kood.Koefitsient,
    price: kood.Hind,
    analysis_group_id: analysisGroupId,
  }));

  const analysisGroupItems = toArray(analysisGroup.Uuring);

  for (const item of analysisGroupItems) {
    const analysisElement = item.UuringuElement;

    const { data: insertedAnalysisElement, error } = await supabase
      .from('analysis_elements')
      .upsert(
        {
          analysis_id_oid: analysisElement.UuringIdOID,
          analysis_id_original: analysisElement.UuringId,
          tehik_short_loinc: analysisElement.TLyhend,
          tehik_loinc_name: analysisElement.KNimetus,
          analysis_name_lab: analysisElement.UuringNimi,
          order: analysisElement.Jarjekord,
          parent_analysis_group_id: analysisGroupId,
          material_groups: toArray(item.MaterjalideGrupp),
        },
        { onConflict: 'analysis_id_original', ignoreDuplicates: false },
      )
      .select('id');

    if (error || !insertedAnalysisElement[0]?.id) {
      throw new Error(
        `Failed to insert analysis element (id: ${analysisElement.UuringId}), error: ${error?.message}`,
      );
    }

    const insertedAnalysisElementId = insertedAnalysisElement[0].id;

    if (analysisElement.Kood) {
      const analysisElementCodes = toArray(analysisElement.Kood);
      codes.push(
        ...analysisElementCodes.map((kood) => ({
          hk_code: kood.HkKood,
          hk_code_multiplier: kood.HkKoodiKordaja,
          coefficient: kood.Koefitsient,
          price: kood.Hind,
          analysis_element_id: insertedAnalysisElementId,
        })),
      );
    }

    const analyses = analysisElement.UuringuElement;
    if (analyses?.length) {
      for (const analysis of analyses) {
        const { data: insertedAnalysis, error } = await supabase
          .from('analyses')
          .upsert(
            {
              analysis_id_oid: analysis.UuringIdOID,
              analysis_id_original: analysis.UuringId,
              tehik_short_loinc: analysis.TLyhend,
              tehik_loinc_name: analysis.KNimetus,
              analysis_name_lab: analysis.UuringNimi,
              order: analysis.Jarjekord,
              parent_analysis_element_id: insertedAnalysisElementId,
            },
            { onConflict: 'analysis_id_original', ignoreDuplicates: false },
          )
          .select('id');

        if (error || !insertedAnalysis[0]?.id) {
          throw new Error(
            `Failed to insert analysis (id: ${analysis.UuringId}) error: ${error?.message}`,
          );
        }

        const insertedAnalysisId = insertedAnalysis[0].id;
        if (analysisElement.Kood) {
          const analysisCodes = toArray(analysis.Kood);

          codes.push(
            ...analysisCodes.map((kood) => ({
              hk_code: kood.HkKood,
              hk_code_multiplier: kood.HkKoodiKordaja,
              coefficient: kood.Koefitsient,
              price: kood.Hind,
              analysis_id: insertedAnalysisId,
            })),
          );
        }
      }
    }
  }

  const { error: codesError } = await supabase
    .from('codes')
    .upsert(codes, { ignoreDuplicates: false });

  if (codesError?.code) {
    throw new Error(
      `Failed to insert codes (analysis group id: ${analysisGroup.UuringuGruppId})`,
    );
  }
}

export async function syncPublicMessage(
  message?: MedipostPublicMessageResponse | null,
) {
  const supabase = createCustomClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!,
    {
      auth: {
        persistSession: false,
        autoRefreshToken: false,
        detectSessionInUrl: false,
      },
    },
  );

  try {
    const providers = toArray(message?.Saadetis?.Teenused.Teostaja);
    const analysisGroups = providers.flatMap((provider) =>
      toArray(provider.UuringuGrupp),
    );
    if (!message || !analysisGroups.length) {
      return supabase.schema('audit').from('sync_entries').insert({
        operation: 'ANALYSES_SYNC',
        comment: 'No data received',
        status: SyncStatus.Fail,
        changed_by_role: 'service_role',
      });
    }

    for (const analysisGroup of analysisGroups) {
      await saveAnalysisGroup(analysisGroup, supabase);
    }

    await supabase.schema('audit').from('sync_entries').insert({
      operation: 'ANALYSES_SYNC',
      status: SyncStatus.Success,
      changed_by_role: 'service_role',
    });
  } catch (e) {
    console.error(e);
    await supabase
      .schema('audit')
      .from('sync_entries')
      .insert({
        operation: 'ANALYSES_SYNC',
        status: SyncStatus.Fail,
        comment: JSON.stringify(e),
        changed_by_role: 'service_role',
      });
  }
}

// TODO use actual parameters
export async function composeOrderXML(
  /*  chosenAnalysisElements?: number[],
  chosenAnalyses?: number[], */
  comment?: string,
) {
  const supabase = createCustomClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!,
    {
      auth: {
        persistSession: false,
        autoRefreshToken: false,
        detectSessionInUrl: false,
      },
    },
  );

  // TODO remove dummy when actual implemetation is present
  const orderedElements = [1, 75];
  const orderedAnalyses = [10, 11, 100];

  const createdAnalysisOrder = {
    id: 4,
    user_id: 'currentUser.user?.id',
    analysis_element_ids: orderedElements,
    analysis_ids: orderedAnalyses,
    status: AnalysisOrderStatus[1],
    created_at: new Date(),
  };

  const { data: analysisElements } = (await supabase
    .from('analysis_elements')
    .select(`*, analysis_groups(*)`)
    .in('id', orderedElements)) as {
    data: ({
      analysis_groups: Tables<'analysis_groups'>;
    } & Tables<'analysis_elements'>)[];
  };
  const { data: analyses } = (await supabase
    .from('analyses')
    .select(`*, analysis_elements(*, analysis_groups(*))`)
    .in('id', orderedAnalyses)) as {
    data: ({
      analysis_elements: Tables<'analysis_elements'> & {
        analysis_groups: Tables<'analysis_groups'>;
      };
    } & Tables<'analyses'>)[];
  };

  const analysisGroups: Tables<'analysis_groups'>[] = uniqBy(
    (
      analysisElements?.flatMap(({ analysis_groups }) => analysis_groups) ?? []
    ).concat(
      analyses?.flatMap(
        ({ analysis_elements }) => analysis_elements.analysis_groups,
      ) ?? [],
    ),
    'id',
  );

  const specimenSection = [];
  const analysisSection = [];
  let order = 1;
  for (const currentGroup of analysisGroups) {
    let relatedAnalysisElement = analysisElements?.find(
      (element) => element.analysis_groups.id === currentGroup.id,
    );
    const relatedAnalyses = analyses?.filter((analysis) => {
      return analysis.analysis_elements.analysis_groups.id === currentGroup.id;
    });

    if (!relatedAnalysisElement) {
      relatedAnalysisElement = relatedAnalyses?.find(
        (relatedAnalysis) =>
          relatedAnalysis.analysis_elements.analysis_groups.id ===
          currentGroup.id,
      )?.analysis_elements;
    }

    if (!relatedAnalysisElement || !relatedAnalysisElement.material_groups) {
      throw new Error(
        `Failed to find related analysis element for group ${currentGroup.name} (id: ${currentGroup.id})`,
      );
    }

    for (const group of relatedAnalysisElement?.material_groups as MaterjalideGrupp[]) {
      const materials = toArray(group.Materjal);
      const specimenXml = materials.flatMap(
        ({ MaterjaliNimi, MaterjaliTyyp, MaterjaliTyypOID, Konteiner }) => {
          return toArray(Konteiner).map((container) =>
            getSpecimen(
              MaterjaliTyypOID,
              MaterjaliTyyp,
              MaterjaliNimi,
              order,
              container.ProovinouKoodOID,
              container.ProovinouKood,
            ),
          );
        },
      );

      specimenSection.push(...specimenXml);
    }

    const groupXml = getAnalysisGroup(
      currentGroup.original_id,
      currentGroup.name,
      order,
      relatedAnalysisElement,
    );
    order++;
    analysisSection.push(groupXml);
  }

  // TODO get actual data when order creation is implemented
  return `<?xml version="1.0" encoding="UTF-8"?>
  <Saadetis xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="TellimusLOINC.xsd">
  ${getPais(process.env.MEDIPOST_USER!, process.env.MEDIPOST_RECIPIENT!, createdAnalysisOrder.created_at, createdAnalysisOrder.id)}
  <Tellimus cito="EI">
		<ValisTellimuseId>${createdAnalysisOrder.id}</ValisTellimuseId>
		<!--<TellijaAsutus>-->
	  ${getClientInstitution()}
		<!--<TeostajaAsutus>-->
		${getProviderInstitution()}
		<!--<TellijaIsik>-->
    ${getClientPerson()}
		<!--<SisestajaIsik>-->
    ${getOrderEnteredByPerson()}
		<TellijaMarkused>${comment ?? ''}</TellijaMarkused>
		${getPatient(49610230861, 'Surname', 'First Name', '1996-10-23', 'N')}
		${getConfidentiality()}
    ${specimenSection.join('')}
    ${analysisSection?.join('')}
	</Tellimus>
</Saadetis>`;
}

function getLatestMessage(messages?: Message[]) {
  if (!messages?.length) {
    return null;
  }

  return messages.reduce((prev, current) =>
    Number(prev.messageId) > Number(current.messageId) ? prev : current,
  );
}

export async function syncPrivateMessage(
  parsedMessage?: MedipostOrderResponse,
) {
  const supabase = createCustomClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!,
    {
      auth: {
        persistSession: false,
        autoRefreshToken: false,
        detectSessionInUrl: false,
      },
    },
  );

  const response = parsedMessage?.Saadetis?.Vastus;

  if (!response) {
    throw new Error(`Invalid data in private message response`);
  }

  const status = response.TellimuseOlek;

  const { data: analysisOrder, error: analysisOrderError } = await supabase
    .from('analysis_orders')
    .select('user_id')
    .eq('id', response.ValisTellimuseId);

  if (analysisOrderError || !analysisOrder?.[0]?.user_id) {
    throw new Error(
      `Could not find analysis order with id ${response.ValisTellimuseId}`,
    );
  }

  const { data: analysisResponse, error } = await supabase
    .from('analysis_responses')
    .upsert(
      {
        analysis_order_id: response.ValisTellimuseId,
        order_number: response.TellimuseNumber,
        order_status: AnalysisOrderStatus[status],
        user_id: analysisOrder[0].user_id,
      },
      { onConflict: 'order_number', ignoreDuplicates: false },
    )
    .select('id');

  if (error || !analysisResponse?.[0]?.id) {
    throw new Error(
      `Failed to insert or update analysis order response (external id: ${response?.TellimuseNumber})`,
    );
  }
  const analysisGroups = toArray(response.UuringuGrupp);

  const responses: Omit<
    Tables<'analysis_response_elements'>,
    'id' | 'created_at' | 'updated_at'
  >[] = [];
  for (const analysisGroup of analysisGroups) {
    const groupItems = toArray(
      analysisGroup.Uuring as ResponseUuringuGrupp['Uuring'],
    );
    for (const item of groupItems) {
      const element = item.UuringuElement;
      const elementAnalysisResponses = toArray(element.UuringuVastus);

      responses.push(
        ...elementAnalysisResponses.map((response) => ({
          analysis_element_original_id: element.UuringId,
          analysis_response_id: analysisResponse[0]!.id,
          norm_lower: response.NormAlum?.['#text'] ?? null,
          norm_lower_included:
            response.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah',
          norm_status: response.NormiStaatus,
          norm_upper: response.NormYlem?.['#text'] ?? null,
          norm_upper_included:
            response.NormYlem?.['@_kaasaarvatud'].toLowerCase() === 'jah',
          response_time: response.VastuseAeg ?? null,
          response_value: response.VastuseVaartus,
          unit: element.Mootyhik ?? null,
          original_response_element: element,
        })),
      );
    }
  }

  const { error: deleteError } = await supabase
    .from('analysis_response_elements')
    .delete()
    .eq('analysis_response_id', analysisResponse[0].id);

  if (deleteError) {
    throw new Error(
      `Failed to clean up response elements for response id ${analysisResponse[0].id}`,
    );
  }

  const { error: elementInsertError } = await supabase
    .from('analysis_response_elements')
    .insert(responses);

  if (elementInsertError) {
    throw new Error(
      `Failed to insert order response elements for response id ${analysisResponse[0].id}`,
    );
  }

  return AnalysisOrderStatus[status];
}
