package io.tofhir.engine.model

import ch.qos.logback.more.appenders.marker.MapMarker
import io.tofhir.engine.util.TimeUtil

import java.sql.Timestamp

/**
 * Base trait representing the result of a FHIR mapping process.
 */
trait BaseFhirMappingResult {

  /**
   * Identifier of the job that this mapping is performed within
   */
  val jobId: String

  /**
   * The name of the specific mapping task being executed.
   */
  val mappingTaskName: String

  /**
   * Timestamp of the result
   */
  val timestamp: Timestamp

  /**
   * A JSON serialization of the source data. The main data source is represented with the "mainSource" key,
   * while other data sources are serialized using their respective source aliases as keys.
   *
   * Example structure:
   * {
   *  "mainSource": {
   *    ... // Serialized data from the main source
   *  },
   *  "encounters": {
   *    ... // Data from encounters source
   *  },
   *  "observations": {
   *    ... // Data from observations source
   *  }
   * }
   */
  val source: String

  /**
   * If there is a problem in the process, description of the problem
   */
  val error: Option[FhirMappingError]

  /**
   * The optional identifier for FhirMappingJobExecution object
   */
  val executionId: Option[String]

  /**
   * The optional identifier for the project under which this mapping job is being executed.
   */
  val projectId: Option[String]
}

/**
 * Case class representing the result of a FHIR mapping process, extending the base mapping result.
 * This model is designed to store the outcome of the mapping, including the generated FHIR resource.
 *
 * Each instance of `FhirMappingResult` corresponds to a single FHIR resource, regardless
 * of how many FHIR resources are created from each input row.
 *
 * If the mapping is 1-to-many, multiple instances of `FhirMappingResult` will be created from a single source row,
 * each holding one of the generated FHIR resources.
 *
 * It includes the common fields from [[BaseFhirMappingResult]], along with specific information about
 * the FHIR resource type and the serialized output of the mapped resource.
 *
 * @param resourceType       The type of FHIR resource generated by the mapping. This field is optional, as the
 *                           FHIR mapping result may represent a flat structure without a specific FHIR resource type.
 * @param mappedFhirResource The mapped FHIR Resource
 */
case class FhirMappingResult(
                              jobId: String,
                              mappingTaskName: String,
                              timestamp: Timestamp,
                              source: String,
                              error: Option[FhirMappingError] = None,
                              executionId: Option[String] = None,
                              projectId: Option[String] = None,
                              resourceType: Option[String] = None,
                              mappedFhirResource: Option[MappedFhirResource] = None
                            ) extends BaseFhirMappingResult {
  final val eventId: String = "MAPPING_RESULT"

  override def toString: String = {
    s"Mapping failure (${error.get.code}) for job '$jobId' and mappingTask '$mappingTaskName'${mappedFhirResource.flatMap(_.mappingExpr).map(e => s" within expression '$e'").getOrElse("")} execution '${executionId.getOrElse("")}'!\n" +
      s"\tSource: $source\n" +
      s"\tError: ${error.get.description}" +
      error.get.expression.map(e => s"\n\tExpression: $e").getOrElse("")
  }

  /**
   * Converts the FhirMappingResult to a MapMarker.
   *
   * @return The MapMarker object representing the FhirMappingResult.
   */
  def toMapMarker: MapMarker = {
    // create a new HashMap to store the marker attributes
    val markerMap: java.util.Map[String, Any] = new java.util.HashMap[String, Any]()
    // add attributes to the marker map
    markerMap.put("jobId", jobId)
    markerMap.put("projectId", projectId.getOrElse(""))
    markerMap.put("executionId", executionId.getOrElse(""))
    markerMap.put("mappingTaskName", mappingTaskName)
    markerMap.put("mappingExpr", mappedFhirResource.flatMap(_.mappingExpr).orNull)
    markerMap.put("source", source)
    markerMap.put("errorCode", error.get.code)
    markerMap.put("errorDesc", error.get.description)
    markerMap.put("errorExpr", error.get.expression.getOrElse(""))
    markerMap.put("eventId", eventId)
    // Set the result to "STARTED" to ensure proper display in the Kibana dashboard,
    // preventing the display of a "-" in the relevant column when the result is not yet available.
    markerMap.put("result", FhirMappingJobResult.STARTED)
    // The current timestamp is automatically added to the log entry when it is sent to Elasticsearch or written to a file.
    // As a result, there is no need to manually add a "@timestamp" field.
    // However, during the process of writing the log to Elasticsearch, the timestamp is rounded, resulting in a loss of precision.
    // For example, "2024-08-28_13:54:44.740" may be rounded to "2024-08-28_13:54:44.000" in Elasticsearch.
    // This rounding leads to the loss of crucial millisecond information, which is important for accurately sorting logs.
    markerMap.put("@timestamp", TimeUtil.getCurrentISOTime)
    // create a new MapMarker using the marker map
    val marker: MapMarker = new MapMarker("marker", markerMap)
    // add mappedResource to the marker map if error code is INVALID_RESOURCE
    if (mappedFhirResource.isDefined && error.get.code == FhirMappingErrorCodes.INVALID_RESOURCE)
      markerMap.put("mappedResource", mappedFhirResource.get.mappedResource.get)
    marker
  }
}

/**
 * Represents the result of the mapping process for a single source row, storing the generated FHIR resources.
 * Each source row corresponds to one instance of `FhirMappingResultsForInput`. If the mapping process is
 * 1-to-many, the `mappedFhirResources` field contains all the FHIR resources created from the single source row.
 *
 * In addition to the common fields from [[BaseFhirMappingResult]], it includes a collection of mapped
 * FHIR resources, which represent the outcome of the mapping process for each input source.
 *
 * @param mappedFhirResources List of mapped FHIR resources. This is used to group mapped resources by input row.
 */
case class FhirMappingResultsForInput(
                                       jobId: String,
                                       mappingTaskName: String,
                                       timestamp: Timestamp,
                                       source: String,
                                       error: Option[FhirMappingError] = None,
                                       executionId: Option[String] = None,
                                       projectId: Option[String] = None,
                                       mappedFhirResources: Seq[MappedFhirResource] = Seq.empty
                                     ) extends BaseFhirMappingResult

/**
 * Description of the error occurred during the mapping process
 *
 * @param code        Code for the error (category)
 * @param description Description of the problem
 * @param expression  Mapping expression that problem is occurred (e.g. FHIR Path expression)
 */
case class FhirMappingError(code: String, description: String, expression: Option[String] = None)

/**
 * List of error codes in the mapping process
 */
object FhirMappingErrorCodes {
  //If the input data is invalid (not conforming to given schema)
  final val INVALID_INPUT = "invalid_input"
  // If the mapping process is timed out
  final val MAPPING_TIMEOUT = "mapping_timeout"
  // If there is any problem during execution of mapping expressions
  final val MAPPING_ERROR = "mapping_error"
  // FHIR server rejects the resource creation as it is invalid
  final val INVALID_RESOURCE = "invalid_resource"
  // Timeout in FHIR interaction to create the resource
  final val FHIR_API_TIMEOUT = "fhir_api_timeout"
  // Problem in Terminology or Identity service interactions
  final val SERVICE_PROBLEM = "service_error"

  final val UNEXPECTED_PROBLEM = "unexpected_problem"
}

/**
 * Mapped FHIR resources model after the mapping process, only used for mapping testing feature
 *
 * @param mappingExpr     Mapping expression name that this mapping is based on
 * @param mappedResource  JSON serialization of the FHIR resource generated via the mapping
 * @param fhirInteraction FHIR interaction details
 */
case class MappedFhirResource(mappingExpr: Option[String] = None,
                              mappedResource: Option[String] = None,
                              fhirInteraction: Option[FhirInteraction] = None)

object FhirMappingResultConverter {

  /**
   * Converts a group of FHIR mapping results into a FhirMappingResultsForInput instance.
   * This method aggregates the individual mapping results for a specific input source.
   *
   * @param source The input source corresponding to the group of FHIR mapping results.
   * @param iter   An iterator of FhirMappingResult instances representing the mapping results for this source.
   * @return A FhirMappingResultsForInput containing the aggregated results.
   * @throws IllegalArgumentException if the iterator is empty.
   */
  def convertToFhirMappingResultsForInput(source: String, iter: Iterator[FhirMappingResult]): FhirMappingResultsForInput = {
    // Convert the iterator to a sequence
    val results: Seq[FhirMappingResult] = iter.toSeq

    // Ensure there is at least one result
    val firstResult = results.headOption.getOrElse(
      throw new IllegalArgumentException("There is no mapping result to convert!")
    )

    // Create FhirMappingResultsForInput with the grouped results
    FhirMappingResultsForInput(
      jobId = firstResult.jobId,
      mappingTaskName = firstResult.mappingTaskName,
      timestamp = firstResult.timestamp,
      source = source, // Set the source for the aggregated results
      error = firstResult.error,
      executionId = firstResult.executionId,
      projectId = firstResult.projectId,
      mappedFhirResources = results.flatMap(_.mappedFhirResource)
    )
  }
}