package gov.cms.grouper.snf.app;

import gov.cms.grouper.snf.SnfComponentVersion;
import gov.cms.grouper.snf.SnfTables;
import gov.cms.grouper.snf.lego.BlockWithException;
import gov.cms.grouper.snf.lego.SnfComparator;
import gov.cms.grouper.snf.lego.SnfUtils;
import gov.cms.grouper.snf.model.SnfComponentAbstract;
import gov.cms.grouper.snf.model.SnfError;
import gov.cms.grouper.snf.model.SnfProcessError;
import gov.cms.grouper.snf.model.reader.Rai300;
import gov.cms.grouper.snf.model.table.SnfVersionRow;
import gov.cms.grouper.snf.process.SnfValidations;
import gov.cms.grouper.snf.transfer.SnfClaim;
import gov.cms.grouper.snf.util.reader.SnfDataMapper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

public class Pdpm {

  public static final Logger log = LoggerFactory.getLogger(Pdpm.class);

  private final SnfDataMapper mapper;
  private final List<String> errorLines;

  /*
   * Public API
   */
  public Pdpm() {
    this(new ArrayList<>());
  }

  /**
   * Process one MDS assessment through the PDPM grouper.
   *
   * @param line a fixed-length string in accordance with MDS 3.00.1 data specification
   * @return the SnfClaim object with grouping results or <code>null</code> if encountered issues
   *         Below are options to obtain the calculated results from the SnfClaim: - getHippsCode()
   *         : String - getErrors() : List<String> - getVersion() : int
   */
  public SnfClaim exec(String line) {
    SnfClaim claim = this.transformToClaim(line);
    return this.process(claim);
  }

  /**
   * Process one MDS assessment through the PDPM grouper. Method is used in example C++ code for
   * calling the jar.
   *
   * @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();
    SnfClaim claim = pdpm.transformToClaim(line);
    return pdpm.getClaimFixedString(pdpm.process(claim));
  }

  /**
   * 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</code> if encountered issues
   *         Below are options to obtain the calculated results from the SnfClaim: - getHippsCode()
   *         : String - getErrors() : List<String> - getVersion() : int
   */
  public SnfClaim exec(SnfClaim claim) {
    return this.process(claim);
  }

  /**
   * Process one or more MDS assessment through the PDPM grouper.
   *
   * @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</code> if encountered
   *         issues Below are options to obtain the calculated results from the SnfClaim: -
   *         getHippsCode() : String - getErrors() : List<String> - getVersion() : int
   */
  public List<SnfClaim> exec(File snfFile) {
    List<SnfClaim> claims = new ArrayList<>(5000);
    this.processFile(snfFile, (line) -> {
      final SnfClaim claim = this.exec(line);
      claims.add(claim);
      return claim;
    });
    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) {
    this.exec(null, path, additionalPostProcessing);
  }

  public String getClaimFixedString(SnfClaim snfClaim) {
    return this.mapper.mapClaimToMds(snfClaim, Rai300.values());
  }

  /*
   * Protected constructor and methods primarily used for internal logic or testing
   */

  protected Pdpm(List<String> errorLines) {
    this.mapper = new SnfDataMapper();
    this.errorLines = errorLines;
  }

  protected SnfClaim process(SnfClaim claim) {
    this.process(null, claim, null);
    return claim;
  }

  protected <T> void processFile(File file, Function<String, T> lineExec) {
    this.processFile(file.toPath(), lineExec);
  }

  private void closeFile(Scanner sc) {
    sc.close();
  }

  protected Integer getDataVersion(LocalDate ard) {
    Integer version = null;

    for (Entry<Integer, SnfVersionRow> row : SnfTables.versions.entrySet()) {
      if (SnfComparator.betweenInclusive(row.getValue().getFrom(), ard, row.getValue().getTo())) {
        version = row.getKey();
        break;
      }
    }

    return version;
  }

  protected SnfClaim transformToClaim(String line) {
    SnfClaim claim = this.mapper.mapMdsToClaim(line, Rai300.values());
    return claim;
  }

  protected void tryExec(String line, BlockWithException ex) {
    try {
      ex.exec();
    } catch (Throwable th) {
      if (errorLines == null) {
        throw new RuntimeException(th);
      } else {
        errorLines.add(line);
      }
    }

  }

  protected SnfClaim process(Integer dataVersion, SnfClaim claim,
      Consumer<SnfClaim> additionalPostProcessing) {
    tryExec(claim.getOriginalRecord(), () -> {

      SnfComponentVersion releaseVersion = null;
      int correctDataVersion = Integer.MIN_VALUE;
      if (dataVersion == null) {
        correctDataVersion = this.getDataVersion(claim.getAssessmentReferenceDate());
        releaseVersion = SnfComponentVersion.toSnfComponentVersion(correctDataVersion);
      } else {
        correctDataVersion = dataVersion;
        releaseVersion = SnfComponentVersion.toSnfComponentVersion(correctDataVersion);
      }


      claim.setVersion(correctDataVersion);
      claim.setRecalculated_z0100b(SnfTables.getRecalculatedZ0100B());

      if (releaseVersion == null) {
        claim.addErrors(SnfError.INVALID_ASSESSMENT_REFERENCE_DATE.getReason());
      } else if (releaseVersion != null) {
        try (SnfComponentAbstract snfComponent =
            releaseVersion.getCreateComponent().apply(correctDataVersion)) {
          // has to call process in order for KesselRun to work
          snfComponent.process(claim);
        } catch (SnfProcessError se) {
          log.debug(se.getMessage());
          throw se;
        } catch (Throwable th) {
          log.debug(th.getMessage());
          claim.addErrors(SnfError.PROCESS_ERROR.getReason(th.getMessage()));
          throw th;
        }
      }
    });

    if (additionalPostProcessing != null) {
      additionalPostProcessing.accept(claim);
    }
    return claim;
  }


  protected SnfClaim exec(Integer version, String line,
      Consumer<SnfClaim> additionalPostProcessing) {
    SnfClaim claim = this.transformToClaim(line);
    this.execClaim(version, claim, additionalPostProcessing);
    return claim;
  }

  protected List<SnfClaim> exec(Integer version, List<String> lines,
      Consumer<SnfClaim> additionalPostProcessing) {
    final List<SnfClaim> results = new ArrayList<>(lines.size());

    for (String line : lines) {
      final SnfClaim claim = this.exec(version, line, additionalPostProcessing);
      results.add(claim);
    }

    return results;

  }

  protected List<SnfClaim> exec(Integer version, List<String> lines) {
    return this.exec(version, lines, null);
  }


  protected SnfClaim exec(Integer version, String line) {
    return this.exec(version, line, null);
  }

  public List<SnfClaim> exec(Integer version, Path path) throws IOException {
    final List<SnfClaim> results = new ArrayList<>(5000);
    this.processFile(path, (line) -> {
      this.exec(version, line, (claim) -> {
        results.add(claim);
      });
      return line;
    });

    return results;
  }

  protected void exec(Integer version, Path path, Consumer<SnfClaim> additionalPostProcessing) {
    this.processFile(path, (line) -> {
      final SnfClaim claim = this.exec(version, line, additionalPostProcessing);
      return claim;
    });

  }

  protected SnfClaim execClaim(Integer version, SnfClaim claim) {
    return this.execClaim(version, claim, null);
  }

  protected SnfClaim execClaim(Integer version, SnfClaim claim,
      Consumer<SnfClaim> additionalPostProcessing) {
    this.validate(claim);
    this.process(version, claim, additionalPostProcessing);
    return claim;
  }


  protected <T> void processFile(Path path, Function<String, T> lineExec) {
    try (Scanner sc = new Scanner(path, "UTF-8")) {
      // close the file on JVM shutdown
      Thread closeFileThread = new Thread() {
        public void run() {
          log.debug("closing files");
          SnfUtils.tryIgnoreExcption(() -> closeFile(sc));
        }
      };

      Runtime.getRuntime().addShutdownHook(closeFileThread);

      int counter = 1;

      while (sc.hasNextLine()) {
        if (counter % 10000 == 0) {
          System.out.println("lines processed: " + counter);
        }
        String line = sc.nextLine();
        if (sc.ioException() != null) {
          throw sc.ioException();
        }
        lineExec.apply(line);
        counter += 1;
      }

    } catch (Throwable th) {
      throw new RuntimeException(th);
    }

  }

  protected void validate(SnfClaim claim) {
    tryExec(claim.getOriginalRecord(), () -> {
      SnfValidations.validateInputs(claim);
    });
  }

}
