package gov.cms.grouper.snf.r2;

import com.mmm.his.cer.foundation.exception.FoundationException;
import gov.cms.grouper.snf.*;
import gov.cms.grouper.snf.model.SnfDiagnosisCode;
import gov.cms.grouper.snf.model.SnfProcessException;
import gov.cms.grouper.snf.model.enums.*;
import gov.cms.grouper.snf.model.table.BasicRow;
import gov.cms.grouper.snf.model.table.ClinicalCategoryMasterRow;
import gov.cms.grouper.snf.model.table.NtaComorbidityMasterRow;
import gov.cms.grouper.snf.model.table.SlpComorbidityMasterRow;
import gov.cms.grouper.snf.r2.logic.*;
import gov.cms.grouper.snf.r2.logic.nursing.BscpLogic;
import gov.cms.grouper.snf.r2.logic.nursing.ReducedPhysicalFunctionLogic;
import gov.cms.grouper.snf.r2.logic.nursing.SpecialCare;
import gov.cms.grouper.snf.transfer.ISnfClaim;
import gov.cms.grouper.snf.util.ClaimInfo;
import gov.cms.grouper.snf.util.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

public class SnfComponent extends SnfComponentAbstract {

  private static final Logger LOG = LoggerFactory.getLogger(SnfComponent.class);

  // Used in reflection
  public SnfComponent() {
    this(100);
  }

  // TODO: This seems like a bad way to do this
  public SnfComponent(int dataVersion) {
    super(dataVersion);
    if (dataVersion < SnfComponentVersion.r2.getFrom()
            && dataVersion > SnfComponentVersion.r2.getTo()) {
      throw new RuntimeException("Version mismatch");
    }
  }

  /**
   * Generate HIPPS code based on CMG from each payment component
   *
   * @param therapyCmg        PT/OT CMG
   * @param speechCmg         SLP CMG
   * @param nursingCmg        Nursing CMG
   * @param ntaCmg            NTA CMG
   * @param assessmentIndicator                 Based on item A0310B, IPA is 0 and 5-Day is 1
   * @return HIPPS codes as five characters string
   */
  protected static String getHIPPSCode(String therapyCmg, String speechCmg,
      NursingCmg nursingCmg, String ntaCmg, Integer assessmentIndicator) {
    String aiCode = (assessmentIndicator == null) ? "" : String.valueOf(assessmentIndicator);
    return String.join("",
        therapyCmg.substring(1),
        speechCmg.substring(1),
        nursingCmg.getHipps(),
        ntaCmg.substring(1),
        aiCode);
  }

    public static List<SnfDiagnosisCode> populate(int version, Iterable<SnfDiagnosisCode> dxCodes) {
    List<SnfDiagnosisCode> result = new ArrayList<>();
    for (SnfDiagnosisCode code : dxCodes) {
      try {
        String trimmedCode = code.getValue().replaceAll("[.^]", "").trim();
        ClinicalCategoryMasterRow clinicalCategoryMasterRow =
            SnfTables.get(SnfTables.clinicalCategoryMasterTable, trimmedCode,
                BasicRow::isVersion, version);
        SlpComorbidityMasterRow slpComorbidityMasterRow =
            SnfTables.get(SnfTables.slpComorbidityMasterTable, trimmedCode,
                BasicRow::isVersion, version);
        Set<NtaComorbidityMasterRow> ntaComorbidityMasterRows =
            SnfTables.getAll(SnfTables.ntaComorbidityMasterTable, trimmedCode,
                BasicRow::isVersion, version);

        // if nta comorbidity table don't have match for the code, pass in null for nta comorbidity
        // if nta comorbidity table has one or more mappings, loop thru each and create a
        // snfDiagnosisCode for each
        String clinicalCategory = clinicalCategoryMasterRow == null ? null : clinicalCategoryMasterRow.getClinicalCategory();
        String slpCategory = slpComorbidityMasterRow == null ? null : slpComorbidityMasterRow
            .getSlpCategory();
        if (ntaComorbidityMasterRows == null) {
          result.add(new SnfDiagnosisCode(trimmedCode, clinicalCategory, slpCategory, null));
        } else {
          ntaComorbidityMasterRows.forEach(ntaComorbidityMasterRow ->
              result.add(new SnfDiagnosisCode(trimmedCode, clinicalCategory, slpCategory,
              ntaComorbidityMasterRow.getNtaCategory())));
        }
      } catch (NullPointerException ex) {
        LOG.warn("Invalid ICD 10 code: {}", code.getValue());
      }
    }
    return result;
  }

  /**
   * Determine whether it is interim payment assessment.
   * OBRA was not used in this version
   * @param ppsValue the PPS assessment value provided.
   * @param obraValue not used
   * @return true if it is interim payment. False otherwise.
   */
  @Override
  public Boolean isInterimPayment(PpsAssessment ppsValue, ObraAssessment obraValue) {
    if (ppsValue == null) return null;
    switch(ppsValue) {
      case FIVE_DAY:
        return false;
      case INTERIM_PAYMENT_ASSESSMENT:
        return true;
      default:
        return null;
    }
  }

  private static void setPpsObraError(ISnfClaim claim) {
    // when execute with snfClaim
    if (claim.getOriginalRecord() == null) {
      claim.addErrors(
              SnfError.INVALID_A0310A_OR_A0310B.getReason(
                String.valueOf(claim.getAssessmentIndicator()),
                String.valueOf(claim.getObra())
              )
      );
    }
    // when execute with fixed length string
    else {
      int begin = Rai300.A0310A.getIndex() - 1;
      int end = begin + Rai300.A0310A.getLength();
      String obraValue = claim.getOriginalRecord().substring(begin, end);

      begin = Rai300.A0310B.getIndex() - 1;
      end = begin + Rai300.A0310B.getLength();
      String ppsValue = claim.getOriginalRecord().substring(begin, end);

      claim.addErrors(SnfError.INVALID_A0310A_OR_A0310B.getReason(obraValue, ppsValue));
    }
  }

  @Override
  public void exec(ISnfClaim claim) {
    SnfContext.trace(claim);

    Boolean hasIPA;
    Optional<PpsAssessment> maybePpsAssessment = PpsAssessment.of(claim.getPps());
    hasIPA = isInterimPayment(maybePpsAssessment.orElse(null), ObraAssessment.of(claim.getObra()).orElse(ObraAssessment.NONE));

    if (hasIPA == null) {
      setPpsObraError(claim);
    }

    // TODO should this just return instead of throwing an exception?
    if (claim.hasError()) {
      throw new SnfProcessException("Claim has errors and cannot process further " + claim.getErrors());
    }

    ClaimInfo asstInfo = ClaimInfo.of(getDataVersion(), hasIPA, claim.getAssessmentMap());
    try {

      String trimmedPdx = claim.getPrimaryDiagnosis().getValue().replaceAll("[.^]", "").trim();
      ClinicalCategoryMasterRow clinicalCategoryMasterRow =
          SnfTables.get(SnfTables.clinicalCategoryMasterTable, trimmedPdx,
              BasicRow::isVersion, this.getDataVersion());
      SlpComorbidityMasterRow slpComorbidityMasterRow =
          SnfTables.get(SnfTables.slpComorbidityMasterTable, trimmedPdx,
              BasicRow::isVersion, this.getDataVersion());
      NtaComorbidityMasterRow ntaComorbidityMasterRow =
          SnfTables.get(SnfTables.ntaComorbidityMasterTable, trimmedPdx,
              BasicRow::isVersion, this.getDataVersion());

      String clinicalCategory = clinicalCategoryMasterRow == null ? null : clinicalCategoryMasterRow.getClinicalCategory();
      String slpCategory = slpComorbidityMasterRow == null ? null : slpComorbidityMasterRow
          .getSlpCategory();
      String ntaCategory = ntaComorbidityMasterRow == null ? null : ntaComorbidityMasterRow
          .getNtaCategory();

      SnfDiagnosisCode pdx = new SnfDiagnosisCode(trimmedPdx, clinicalCategory, slpCategory, ntaCategory);
      claim.setPrimaryDiagnosis(pdx);

    } catch (NullPointerException ex) {
      LOG.warn("Invalid Pdx code: {}", claim.getPrimaryDiagnosis().getValue());
    }

    // If PDX is null/RTP and it's PPS is 5-day assessment & IPA, then blank out HIPPS code
    if (claim.getPrimaryDiagnosis().getClinicalCategory() == null
            || (claim.getPrimaryDiagnosis().getClinicalCategory().equals(ClinicalCategory.RETURN_TO_PROVIDER.getDescription())
            && claim.getPps() != null && !claim.getPps().equals(PpsAssessment.NONE.getValue()))) {
      claim.setHippsCode("");
      LOG.error("PDX clinical category ({}) is null or return to provider and PPS ({}) is 5-day assessment or IPA, " +
              "setting HIPPS to blank", claim.getPrimaryDiagnosis().getClinicalCategory(), claim.getPps());
      return;
    }

    List<SnfDiagnosisCode> secondaryDiagnoses =
        SnfComponent.populate(getDataVersion(), claim.getSecondaryDiagnoses());

    ClinicalCategory pdxClinicalCategory = TherapyLogic.getPdxClinicalCategory(
        getDataVersion(), asstInfo, claim.getPrimaryDiagnosis().getValue());

    if (!claim.hasError()) {
      String therapyLogicCmg =
          new TherapyLogic(getDataVersion(), asstInfo, pdxClinicalCategory).exec();

      CognitiveLevel cognitiveLevel = new CognitiveLevelLogic(asstInfo).exec();
      String speechLogicCmg = new SpeechLogic(getDataVersion(), asstInfo, secondaryDiagnoses,
          cognitiveLevel, pdxClinicalCategory).exec();

      Reference<ReducedPhysicalFunctionLogic> physicRef = new Reference<>();

      SpecialCare specialCare = new SpecialCare(asstInfo);
      BscpLogic bscp = new BscpLogic(asstInfo, physicRef);
      ReducedPhysicalFunctionLogic physical = new ReducedPhysicalFunctionLogic(asstInfo, bscp);
      physicRef.set(physical);
      NursingCmg nursingLogicCmg = new NursingLogic(asstInfo, specialCare, physical, bscp).exec();

      String ntaPaymentCompLogicCmg = new NtaLogic(asstInfo, secondaryDiagnoses).exec();

      String hippsCode = SnfComponent.getHIPPSCode(therapyLogicCmg, speechLogicCmg, nursingLogicCmg,
          ntaPaymentCompLogicCmg, claim.getAssessmentIndicator());

      claim.setHippsCode(hippsCode);
    }
  }

  @Override
  public void close() {
    try {
      super.close();
    } catch (Throwable th) {
      throw new RuntimeException(th);
    }
  }

  @Override
  public SnfError validates(ISnfClaim claim) {
    // SnfValidations valid = new SnfValidations(claim);
    return null;
  }


}
