package gov.cms.grouper.snf.app;

import com.mmm.his.cer.foundation.exception.FoundationException;
import gov.cms.grouper.snf.process.SnfProcessor;
import gov.cms.grouper.snf.process.SnfValidations;
import gov.cms.grouper.snf.transfer.SnfClaim;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.function.Consumer;
import java.util.function.Function;

public class Pdpm {

  private static final Logger LOG = LoggerFactory.getLogger(Pdpm.class);
  private final SnfProcessorProxy processor;
  protected boolean ignoreExceptionOnTryExec;

  public Pdpm() {
    processor = new SnfProcessorProxy();
  }

  // PUBLIC METHODS

  /**
   * Process one fixed-length string MDS assessment through the PDPM grouper. Method is used in example C++ code for
   * calling the jar.
   * Does not support XML structured input.
   *
   * @param line a fixed-length string in accordance with MDS 3.00.1 data specification
   * @return if the claim is a Proxy claim, then it returns the original record with the HIPPS code
   * placed within the record. Otherwise, it will just return the HIPPS Code (5 character string).
   */
  public static String stringExec(String line) {
    Pdpm pdpm = new Pdpm();
    return pdpm.handleLine(line, null).mapClaimToMds();
  }

  /**
   * Process one fixed-length structured string assessment through the PDPM grouper.
   * Does not support XML structured input.
   *
   * @param claimString a fixed-length structured string in accordance with MDS 3.00.1 data specification
   * @return the SnfClaim object with grouping results or {@code null} if encountered issues
   * Below are options to obtain the calculated results from the SnfClaim: - getHippsCode() : String
   * - getErrors() : List&lt;String&gt; - getVersion() : int
   */
  public SnfClaim exec(String claimString) {
    return handleLine(claimString, null);
  }

  /**
   * Process one XML structured string assessment through the PDPM grouper.
   * Does not support fixed-length structured input.
   *
   * @param claimString XML structured string in accordance with MDS 3.00.1 data specification
   * @return the SnfClaim object with grouping results or {@code null} if encountered issues
   *    * Below are options to obtain the calculated results from the SnfClaim: - getHippsCode() : String
   *    * - getErrors() : List&lt;String&gt; - getVersion() : int
   */
  public SnfClaim xmlStringExec(String claimString) {
    return handleXmlString(claimString, null);
  }

  /**
   * Process one MDS assessment through the PDPM grouper.
   *
   * @param claim a SnfClaim object with the necessary inputs set for the PDPM calculation
   * @return the SnfClaim object with grouping results or {@code null} if encountered issues
   * Below are options to obtain the calculated results from the SnfClaim: - getHippsCode() : String
   * - getErrors() : List&lt;String&gt; - getVersion() : int
   */
  public SnfClaim exec(SnfClaim claim) {
    return handleClaim(claim, null);
  }

  /**
   * Process one or more MDS assessment through the PDPM grouper. Accepts fixed-length and XML structured files.
   * XML structured files can only contain one MDS assessment.
   * Fixed-length string structured files can contain multiple MDS assessments.
   *
   * @param snfFile a file containing one or more fixed-length strings representing MDS assessments
   * @return a list of SnfClaim objects with grouping results or {@code null} if encountered
   * issues Below are options to obtain the calculated results from the SnfClaim: - getHippsCode() :
   * String - getErrors() : List&lt;String&gt; - getVersion() : int
   */
  public List<SnfClaim> exec(File snfFile) {
    List<SnfClaim> claims = new ArrayList<>(5000);
    exec(snfFile.toPath(), claims::add);
    return claims;
  }

  /**
   * This is used for Java JDK 32 processing of large data set. Because of memory limitations,
   * client will need to write out the result of SnfClaim into file or other storage in the
   * additionalPostProcessing.
   *
   * @param path                     file path
   * @param additionalPostProcessing processing after processing of claim
   */
  public void exec(Path path, Consumer<SnfClaim> additionalPostProcessing) {
    handleFile(path, additionalPostProcessing);
  }

  // PRIVATE METHODS

  /**
   * Handles fixed-length string style of MDS claim. Expects only a single claim.
   *
   * @param claimString       Single MDS claim
   * @param finalProcessing   processing after processing of claim
   * @return                  SnfClaim parsed from String
   */
  private SnfClaim handleLine(String claimString, Consumer<SnfClaim> finalProcessing) {
    SnfClaim claim = SnfClaim.mapMdsToClaim(claimString);
    return handleClaim(claim, finalProcessing);
  }

  /**
   * Handles XML string style of MDS claim.
   * @param claimString       XML styled MDS claim
   * @param finalProcessing   processing after processing of claim
   * @return                  SnfClaim parsed from String
   */
  private SnfClaim handleXmlString(String claimString, Consumer<SnfClaim> finalProcessing) {
    try {
      SnfClaim claim = SnfClaim.mapXmlStringToClaim(claimString);
      return handleClaim(claim, finalProcessing);
    } catch (Throwable th) {
      throw new RuntimeException(th);
    }
  }

  private SnfClaim handleClaim(SnfClaim claim, Consumer<SnfClaim> finalProcessing) {
    SnfValidations.validateInputs(claim);
    try {
      return processor.process(claim, finalProcessing);
    } catch (Throwable th) {
      if (ignoreExceptionOnTryExec) {
        finalProcessing.accept(claim);
        return claim;
      } else {
        throw new RuntimeException(th);
      }
    }

  }

  private void handleFile(Path path, Consumer<SnfClaim> additionalPostProcessing) {
    if (path.toString().endsWith(".xml")) {
      try {
        LOG.debug("Processing file: " + path.getFileName());
        SnfClaim claim = SnfClaim.mapXmlFileToClaim(path.toString());
        handleClaim(claim, additionalPostProcessing);
      } catch (Throwable th) {
        throw new RuntimeException(th);
      }
    } else {
      handleFixedWidthFile(path, (line) -> handleLine(line, additionalPostProcessing));
    }
  }

  private static <T> void handleFixedWidthFile(Path path, Function<String, T> lineExec) {
    try (Scanner sc = new Scanner(path, "UTF-8")) {
      LOG.debug("Processing file: " + path.getFileName());
      // close the file on JVM shutdown
      Thread closeFileThread = new Thread(() -> {
        LOG.debug("closing files");
        sc.close();
      });

      Runtime.getRuntime().addShutdownHook(closeFileThread);

      while (sc.hasNextLine()) {
        String line = sc.nextLine();
        if (sc.ioException() != null) {
          throw sc.ioException();
        }
        lineExec.apply(line);
      }
    } catch (Throwable th) {
      throw new RuntimeException(th);
    }
  }

  // PRIVATE CLASS PROXIES

  private static class SnfProcessorProxy extends SnfProcessor {
    @Override
    protected SnfClaim process(SnfClaim claim, Consumer<SnfClaim> finalProcessing)
        throws FoundationException, IOException {
      return super.process(claim, finalProcessing);
    }
  }
}
