package org.mitre.synthea.export;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.parser.StrictErrorHandler;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.hapi.ctx.IValidationSupport;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent;
import org.hl7.fhir.r4.model.StructureDefinition;
import org.hl7.fhir.r4.model.ValueSet;
import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceComponent;
import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent;
import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;

/**
 * ValidationSupport provides implementation guide profiles (i.e. StructureDefinitions)
 * to the FHIR validation process. This class does not provide ValueSet expansion.
 */
public class ValidationSupportR4 implements IValidationSupport {
  private static String profileDir = "structureDefinitions/r4";

  private List<IBaseResource> resources;
  private Map<String, IBaseResource> resourcesMap;
  private List<StructureDefinition> definitions;
  private Map<String, StructureDefinition> definitionsMap;
  private Map<String, CodeSystem> codeSystemMap;

  /**
   * Defines the custom validation support for various implementation guides.
   */
  public ValidationSupportR4() {
    resources = new ArrayList<IBaseResource>();
    resourcesMap = new HashMap<String, IBaseResource>();
    definitions = new ArrayList<StructureDefinition>();
    definitionsMap = new HashMap<String, StructureDefinition>();
    codeSystemMap = new HashMap<String, CodeSystem>();

    try {
      loadFromDirectory(profileDir);
    } catch (Throwable t) {
      throw new RuntimeException(t);
    }
  }

  /**
   * Loads the structure definitions from the given directory.
   * @param rootDir the directory to load structure definitions from
   * @return a list of structure definitions
   * @throws Throwable when there is an error reading the structure definitions.
   */
  private void loadFromDirectory(String rootDir) throws Throwable {

    IParser jsonParser = FhirContext.forR4().newJsonParser();
    jsonParser.setParserErrorHandler(new StrictErrorHandler());

    URL profilesFolder = ClassLoader.getSystemClassLoader().getResource(rootDir);
    Path path = Paths.get(profilesFolder.toURI());
    Files.walk(path, Integer.MAX_VALUE).filter(Files::isReadable).filter(Files::isRegularFile)
        .filter(p -> p.toString().endsWith(".json")).forEach(f -> {
          try {
            IBaseResource resource = jsonParser.parseResource(new FileReader(f.toFile()));
            handleResource(resource);
          } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
          }
        });
  }

  private void handleResource(IBaseResource resource) {
    if (resource instanceof Bundle) {
      Bundle bundle = (Bundle) resource;
      for (BundleEntryComponent entry : bundle.getEntry()) {
        if (entry.hasResource()) {
          handleResource(entry.getResource());
        }
      }
    } else {
      resources.add(resource);
      if (resource instanceof CodeSystem) {
        CodeSystem cs = (CodeSystem) resource;
        resourcesMap.put(cs.getUrl(), cs);
        codeSystemMap.put(cs.getUrl(), cs);
      } else if (resource instanceof ValueSet) {
        ValueSet vs = (ValueSet) resource;
        resourcesMap.put(vs.getUrl(), vs);

        if (vs.hasExpansion() && vs.getExpansion().hasContains()) {
          processExpansion(vs.getExpansion().getContains());
        }
      } else if (resource instanceof StructureDefinition) {
        StructureDefinition sd = (StructureDefinition) resource;
        resourcesMap.put(sd.getUrl(), sd);
        definitions.add(sd);
        definitionsMap.put(sd.getUrl(), sd);
      }
    }
  }

  private void processExpansion(List<ValueSetExpansionContainsComponent> list) {
    CodeSystem codeSystem = null;
    for (ValueSetExpansionContainsComponent item : list) {
      if (codeSystemMap.containsKey(item.getSystem())) {
        codeSystem = codeSystemMap.get(item.getSystem());
      } else {
        codeSystem = new CodeSystem();
        codeSystem.setId(item.getSystem());
        codeSystem.setUrl(item.getSystem());
        codeSystemMap.put(item.getSystem(), codeSystem);
      }

      ConceptDefinitionComponent concept = new ConceptDefinitionComponent();
      concept.setCode(item.getCode());
      concept.setDisplay(item.getDisplay());
      codeSystem.addConcept(concept);
    }
  }

  @Override
  public ValueSetExpansionOutcome expandValueSet(FhirContext theContext,
      ConceptSetComponent theInclude) {
    if (theInclude.getSystem().equals("urn:iso:std:iso:4217")) {
      ValueSet valueset =  (ValueSet) resourcesMap.get("http://hl7.org/fhir/ValueSet/currencies");
      ValueSetExpansionOutcome expansion = new ValueSetExpansionOutcome(valueset);
      return expansion;
    }
    return null;
  }

  @Override
  public List<IBaseResource> fetchAllConformanceResources(FhirContext theContext) {
    return resources;
  }

  @Override
  public List<StructureDefinition> fetchAllStructureDefinitions(FhirContext theContext) {
    return definitions;
  }

  @Override
  public CodeSystem fetchCodeSystem(FhirContext theContext, String theSystem) {
    return codeSystemMap.get(theSystem);
  }

  @SuppressWarnings("unchecked")
  @Override
  public <T extends IBaseResource> T fetchResource(FhirContext theContext,
      Class<T> theClass, String theUri) {
    return (T) resourcesMap.get(theUri);
  }

  @Override
  public StructureDefinition fetchStructureDefinition(FhirContext theCtx, String theUrl) {
    return definitionsMap.get(theUrl);
  }

  @Override
  public boolean isCodeSystemSupported(FhirContext theContext, String theSystem) {
    return codeSystemMap.containsKey(theSystem);
  }

  @Override
  public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem,
      String theCode, String theDisplay, String theValueSetUrl) {
    IssueSeverity severity = IssueSeverity.WARNING;
    String message = "Unsupported CodeSystem";

    if (isCodeSystemSupported(theContext, theCodeSystem)) {
      severity = IssueSeverity.ERROR;
      message = "Code not found";

      CodeSystem cs = codeSystemMap.get(theCodeSystem);
      for (ConceptDefinitionComponent def : cs.getConcept()) {
        if (def.getCode().equals(theCode)) {
          if (def.getDisplay() != null && theDisplay != null) {
            if (def.getDisplay().equals(theDisplay)) {
              severity = IssueSeverity.INFORMATION;
              message = "Validated Successfully";
            } else {
              severity = IssueSeverity.WARNING;
              message = "Validated Code; Display mismatch";
            }
          } else {
            severity = IssueSeverity.WARNING;
            message = "Validated Code; No display";
          }
        }
      }
    }

    ValueSet vs = fetchValueSet(theContext, theValueSetUrl);
    if (vs != null && vs.hasCompose() && vs.getCompose().hasExclude()) {
      for (ConceptSetComponent exclude : vs.getCompose().getExclude()) {
        if (exclude.getSystem().equals(theCodeSystem) && exclude.hasConcept()) {
          for (ConceptReferenceComponent concept : exclude.getConcept()) {
            if (concept.getCode().equals(theCode)) {
              severity = IssueSeverity.ERROR;
              message += "; Code Excluded from ValueSet";
            }
          }
        }
      }
    }

    return new CodeValidationResult(severity, message);
  }

  @Override
  public LookupCodeResult lookupCode(FhirContext theContext, String theSystem, String theCode) {
    if (isCodeSystemSupported(theContext, theSystem)) {
      LookupCodeResult result = new LookupCodeResult();
      result.setSearchedForSystem(theSystem);
      result.setSearchedForCode(theCode);
      result.setFound(false);

      CodeSystem cs = codeSystemMap.get(theSystem);
      for (ConceptDefinitionComponent def : cs.getConcept()) {
        if (def.getCode().equals(theCode)) {
          result.setCodeDisplay(def.getDisplay());
          result.setFound(true);
          return result;
        }
      }
    }
    return LookupCodeResult.notFound(theSystem, theCode);
  }

  @Override
  public ValueSet fetchValueSet(FhirContext theContext, String uri) {
    return (ValueSet) resourcesMap.get(uri);
  }

  @Override
  public StructureDefinition generateSnapshot(
      StructureDefinition theInput, String theUrl, String theWebUrl, String theProfileName) {
    return null;
  }
}