/**
 * © Copyright Outburn Ltd. 2022-2024 All Rights Reserved
 *   Project name: FUME-COMMUNITY
 */

import jsonata from 'jsonata';

export interface InternalJsonataExpression {
  translateCodeExtract: jsonata.Expression
  translateCodingExtract: jsonata.Expression
  searchSingle: jsonata.Expression
  literal: jsonata.Expression
  initCap: jsonata.Expression
  duplicate: jsonata.Expression
  selectKeys: jsonata.Expression
  omitKeys: jsonata.Expression
  v2normalizeKey: jsonata.Expression
  v2json: jsonata.Expression
  parseFumeExpression: jsonata.Expression
  constructLineIterator: jsonata.Expression
  extractNextLink: jsonata.Expression
  bundleToArrayOfResources: jsonata.Expression
  structureMapsToMappingObject: jsonata.Expression
  aliasResourceToObject: jsonata.Expression
  conceptMapToTable: jsonata.Expression
  createRawPackageIndexObject: jsonata.Expression
  fixPackageIndexObject: jsonata.Expression
  extractCurrentPackagesFromIndex: jsonata.Expression
  checkPackagesMissingFromIndex: jsonata.Expression
  isEmpty: jsonata.Expression
  codeSystemToDictionary: jsonata.Expression
  valueSetExpandDictionary: jsonata.Expression
  testCodeAgainstVS: jsonata.Expression
  testCodingAgainstVS: jsonata.Expression
  testCodeableAgainstVS: jsonata.Expression
};

const expressions: InternalJsonataExpression = {
  translateCodeExtract: jsonata('$mapFiltered.code'),
  translateCodingExtract: jsonata(`
    $result.{
      'system': target,
      'code': code,
      'display': display
    }`),
  searchSingle: jsonata(`(
    $assert(
      $bundle.total <= 1, 
      'The search ' 
      & $bundle.link[relation='self'].url 
      & ' returned multiple matches - criteria is not selective enough'
    );
    $bundle.entry[search.mode='match'][0].resource
  )`),
  literal: jsonata(`(
    $r := $searchSingle($query, $params);
    $r.resourceType = 'OperationOutcome' ? $error($string($r));
    $exists($r.resourceType) ? $r.resourceType & '/' & $r.id : undefined
  )`),
  initCap: jsonata(`(
    $words := $trim($)~>$split(" ");
    ($words.$initCapOnce($))~>$join(' ')
  )`),
  duplicate: jsonata('$join([1..$times].($str))'),
  selectKeys: jsonata('$in.$sift($, function($v, $k) {$k in $skeys})'),
  omitKeys: jsonata('$in.$sift($, function($v, $k) {($k in $okeys)=false})'),
  v2normalizeKey: jsonata(`(
    $cached := $lookup($keyMap, $);
    $exists($cached) = false 
      ? (
        $titleCased := ($split($initCap($replace($,"'", '')), ' ')~>$join);
        $dtmFixed := $titleCased.$replace('Date/Time', 'DateTime') ~> $replace('Date / Time', 'DateTime');
        `.concat(
      // eslint-disable-next-line @typescript-eslint/indent
        // eslint-disable-next-line no-useless-escape, @typescript-eslint/indent
        `$underscored := $replace($dtmFixed, /[-\+".()\\//]/, '_');
        $registerV2key($, $underscored);
        $underscored;
      )
      : ($cached);
    
  )`)),
  v2json: jsonata(`(
    $rawJson := $v2parse($);
    $v2version := $rawJson.segments[0].\`12\`;
  
    $dtToIso := function($dt){(
      $y := $substring($dt,0,4);
      $m := $substring($dt,4,2);
      $d := $substring($dt,6,2);
      $join([$y,$m,$d],'-')
    )};
  
    $dtmToIso := function($dtm){(
      $dt := $dtToIso($dtm);
      $hh := $substring($dtm,8,2);
      $mm := $substring($dtm,10,2);
      $ss := $substring($dtm,12,2);
      $tm := $join([($hh!=''?$hh),($mm!=''?$mm),($ss!=''?$ss)],':');
      $dt & ($tm!=''? 'T' & $tm)
    )};
  
    $parseValue := function($value, $datatype){(    
      $value = '' 
      ? 
        undefined 
      : $value.(
          $datatype = 'DT' ? $dtToIso($) : ($datatype = 'DTM' ? $dtmToIso($) : $)
        )
    )};
  
    $translateSubfield := function($subfield, $datatypeDef, $sfi){(
      $subfieldDef := $datatypeDef.subfields[$sfi];
      $subfieldDesc := $subfieldDef.desc;
      $subfieldDatatype := $subfieldDef.datatype;
      $sfDataTypeDef := $getDatatypeDef($subfieldDatatype, $v2version);
      $isComplex := $count($sfDataTypeDef.subfields)>0;
      $hasChildren := $count($subfield.fields)>0;
  
      $value := (
        $isComplex = false 
        ? $parseValue($subfield.value, $subfieldDatatype)
        : (
          /* it's a complex type */
          $hasChildren 
          ? ( 
            /* input has children */
            $subfield.fields@$subsubfield#$ssfi.$translateSubfield($subsubfield, $sfDataTypeDef, $ssfi){
              $normalizeKey(name): value
            };
          )
          : ( 
            /* input doesn't have children */
            $translateSubfield({'value': $subfield.value}, $sfDataTypeDef, 0){
              $normalizeKey(name): value
            }
          )
        )      
      );
  
      {
        'name': $subfieldDesc,
        'value': $value != {} ? $value
      }
    )};
  
    $translateField := function($field, $segDef, $fieldIndex){(
      $fieldDef := $segDef.fields[$fieldIndex];
      $fieldDef := $exists($fieldDef)=false ? {'name': $segDef.segmentId & ($fieldIndex + 1)} : $fieldDef;
      $fieldDesc := $fieldDef.desc ? $fieldDef.desc : $fieldDef.name;
      $fieldDesc := $type($fieldDesc) = 'string' and $startsWith($fieldDesc,'Set ID - ') and $fieldIndex=0 ? 'SetID' : $fieldDesc;
      $fieldDatatype := $fieldDef.datatype;
      $datatypeDef := $getDatatypeDef($fieldDatatype, $v2version);
      $isEnc := $segDef.segmentId='MSH' and $fieldIndex=1;
      $isComplex := $count($datatypeDef.subfields)>0;
      $hasChildren := $count($field.fields)>0;
  
      $value := (
        $isEnc ? $field.value : (
          $isComplex = false 
          ? $parseValue($field.value, $fieldDatatype)
          : (
            /* it's a complex type */
            $hasChildren 
            ? ( 
              /* input has children */
              $field.fields@$subfield#$sfi.$translateSubfield($subfield, $datatypeDef, $sfi){
                $normalizeKey(name): value
              };
            )
            : ( 
              /* input doesn't have children */
              $translateSubfield({'value': $field.value}, $datatypeDef, 0){
                $normalizeKey(name): value
              }
            )
          )
        )
      );
      $value := $value = {} ? undefined : $value;
      
      {
        'name': $fieldDesc,
        'value': $value
      };
    )};
  
    $translateSegment := function($segment){(
      $line := $segment.MessageLine;
      $segId := $segment.\`0\`;
      $segDef := $getSegmentDef($segId, $v2version);
      $segment.fields#$i[$i>0].$translateField($, $segDef, $i-1){
        'SegmentDescription': $segDef.desc,
        'MessageLine': $line,
        $normalizeKey(name): value
      }
    )};
    
    $segmentsWithLines := $rawJson.segments#$line.($merge([$, {'MessageLine': $line+1}]));

    $segmentsWithLines@$s.$translateSegment($s){
      $s.\`0\`: $
    };
  )`),
  parseFumeExpression: jsonata(`
    (
      $lines:=[$splitLineFunc($expr)];
      $lines:=$lines[$not($startsWith($trim($),"*") and $endsWith($trim($), "undefined"))];
      $lines:=$append($lines,"");
      $join(
        (
          ($lines)#$i.$lineParser($, $i, $lines)
        ),
        "\r\n"
      )
    )`
  ),
  constructLineIterator: jsonata(`
    (
      $first := $nodes[0].$construct($prefix, $, '', $context, true);
      $middle := $nodes#$i[$i>0 and $i<($count($nodes)-1)].$construct('', $, '', '', true);
      $last := $nodes[-1].$construct('', $, $value, '', false);
      $join([
        $first,
        $middle,
        $last
      ])
    )`
  ),
  extractNextLink: jsonata('link[relation=\'next\'].url'),
  bundleToArrayOfResources: jsonata('[$bundleArray.entry.resource]'),
  structureMapsToMappingObject: jsonata(`
    ($[
      resourceType='StructureMap' 
      and status='active' 
      and useContext[
        code.system = 'http://snomed.info/sct'
        and code.code = '706594005'
      ].valueCodeableConcept.coding[
        system = 'http://codes.fume.health'
      ].code = 'fume'
    ]){
      id: group[name = 'fumeMapping'].rule[name='evaluate'].extension[url = 'http://fhir.fume.health/StructureDefinition/mapping-expression'].valueExpression.expression
    }`
  ),
  aliasResourceToObject: jsonata('group.element{code: target.code}'),
  conceptMapToTable: jsonata(`(
    $cm := (resourceType='Bundle' ? [entry[0].resource] : [$]);
  
    $merge(
      $cm#$i.id.{
        $: $merge(
          $distinct($cm[$i].group.element.code).(
            $code := $;
            {
              $code: $cm[$i].group.element[code=$code].target[
                equivalence='equivalent' 
                or equivalence='equal' 
                or equivalence='wider' 
                or equivalence='subsumes'
                or equivalence='relatedto'
              ].code.{
                "code": $, 
                "source": %.%.%.source, 
                "target": %.%.%.target,
                "display": %.display
              }[]
            }
          )
        )
      }
    )
  )`),
  createRawPackageIndexObject: jsonata(`
  (
    $packageReplace := $replace(?,'#', '@');
  
    $packages := \${$packageReplace(package): $omitKeys($, ["package", "packageIndex"])};
  
    $files := ($.packageIndex.files[resourceType in ['StructureDefinition','ValueSet','CodeSystem','ConceptMap']].(
      $fullPath := $pathJoin(%.%.path, filename);
      $fhirVersion := ($exists(%.%.packageManifest.fhirVersions) ? %.%.packageManifest.fhirVersions : %.%.packageManifest.\`fhir-version-list\`);
      {
        'packageId': %.%.$packageReplace(package),
        'packageName': %.%.packageManifest.name,
        'packageVersion': %.%.packageManifest.version,
        'filename': filename,
        'path': $fullPath,
        'fhirVersion': $fhirVersion,
        'resourceType': resourceType,
        'id': id,
        'url': url,
        'name': name,
        'version': version,
        'kind': kind,
        'type': type,
        'baseDefinition': baseDefinition,
        'derivation': derivation,
        'date': date
      }
    )){path: $};
  
    {
      'packages': $packages,
      'files': $files
    }.(
      $structureDefinitions := files.*[resourceType='StructureDefinition'];
      $valueSets := files.*[resourceType='ValueSet'];
      $codeSystems := files.*[resourceType='CodeSystem'];
      $conceptMaps := files.*[resourceType='ConceptMap'];
      $fhirVersions := $distinct(files.*.fhirVersion);
      $packages := packages;
      $files := files;
  
      $splitVersionId := function($versionId) {(
        $parts := $split($versionId, '.');
        $major := $parts[0];
        $minor := $parts[1];
        $patch := $substringBefore($parts[2], '-');
        $label := $substringAfter($parts[2], '-');
        {
          'id': $versionId,
          'major': $isNumeric($major) ? $number($major) : 0,
          'minor': $isNumeric($minor) ? $number($minor) : 0,
          'patch': $isNumeric($patch) ? $number($patch) : 0,
          'label': $isNumeric($major)=false ? $major : $label != $patch ? $label : ''
        }
      )};
  
      $toMinorVersionId := function($versionId){
        $splitVersionId($versionId).($join([$string(major), $string(minor)], '.'))
      };
  
      $minorVersions := $distinct($fhirVersions.$toMinorVersionId($));
  
      $minorVersions{
        'packages': $packages,
        'files': $files,
        $: (
          $mv := $;
          {
            'structureDefinitions': (
              $sdefs := $files.*[resourceType = 'StructureDefinition' and $mv in (fhirVersion.$toMinorVersionId($))];
              {
                'byUrl': $sdefs{
                  url: path,
                  url & '|' & version: path
                },
                'byId': $sdefs{
                  id: path,
                  id & '|' & version: path
                },
                'byName': $sdefs{
                  name: path,
                  name & '|' & version: path
                }
              }
            ),
            'codeSystems': (
              $sdefs := $files.*[resourceType = 'CodeSystem' and $mv in (fhirVersion.$toMinorVersionId($))];
              {
                'byUrl': $sdefs{
                  url: path,
                  url & '|' & version: path
                },
                'byId': $sdefs{
                  id: path,
                  id & '|' & version: path
                },
                'byName': $sdefs{
                  name: path,
                  name & '|' & version: path
                }
              }
            ),
            'valueSets': (
              $sdefs := $files.*[resourceType = 'ValueSet' and $mv in (fhirVersion.$toMinorVersionId($))];
              {
                'byUrl': $sdefs{
                  url: path,
                  url & '|' & version: path
                },
                'byId': $sdefs{
                  id: path,
                  id & '|' & version: path
                },
                'byName': $sdefs{
                  name: path,
                  name & '|' & version: path
                }
              }
            ),
            'conceptMaps': (
              $sdefs := $files.*[resourceType = 'ConceptMap' and $mv in (fhirVersion.$toMinorVersionId($))];
              {
                'byUrl': $sdefs{
                  url: path,
                  url & '|' & version: path
                },
                'byId': $sdefs{
                  id: path,
                  id & '|' & version: path
                },
                'byName': $sdefs{
                  name: path,
                  name & '|' & version: path
                }
              }
            )
          }
        )
      }
    )
  )`),
  fixPackageIndexObject: jsonata(`(
    $splitVersionId := function($versionId) {(
      $parts := $split($versionId, '.');
      $major := $parts[0];
      $minor := $parts[1];
      $patch := $substringBefore($parts[2], '-');
      $label := $substringAfter($parts[2], '-');
      {
        'id': $versionId,
        'major': $isNumeric($major) ? $number($major) : 0,
        'minor': $isNumeric($minor) ? $number($minor) : 0,
        'patch': $isNumeric($patch) ? $number($patch) : 0,
        'label': $isNumeric($major)=false ? $major : $label != $patch ? $label : ''
      }
    )};

    $splitPackageVersion := function($filesEntry) {
        (
            $merge([$filesEntry, {'packageVersion': $splitVersionId($filesEntry.packageVersion)}])
        )
    };
    
    $bestFileByUrl := function($filePaths) {
        (
            $filesEntries := $filePaths@$fp.$splitPackageVersion($lookup($$.files, $fp));
            $sortedEntries := $filesEntries^(>date, packageName, >packageVersion.major, >packageVersion.minor, >packageVersion.patch, packageVersion.label);
            $sortedEntries[0].path;
        )
    };

    $bestFileById := function($filePaths) {
        (
            $filesEntries := $filePaths@$fp.$splitPackageVersion($lookup($$.files, $fp));
            $urls := $distinct($filesEntries.url);
            $urls@$url.$bestFileByUrl($filesEntries[url=$url].path);
        )
    };

    $bestFileByName := function($filePaths) {
        (
            $filesEntries := $filePaths@$fp.$splitPackageVersion($lookup($$.files, $fp));
            $urls := $distinct($filesEntries.url);
            $urls@$url.$bestFileByUrl($filesEntries[url=$url].path);
        )
    };

    $getDuplicates := function($obj) {
        $sift($obj, function($value) {$type($value) = 'array'})
    };

    $fixTypeByUrl := function($byUrl) {
        (
            $dups := $getDuplicates($byUrl);
            $dupsFixed := $keys($dups){
                $: $bestFileByUrl($lookup($dups, $))
            };
            $merge([$byUrl,$dupsFixed])
        )
    };

    $fixTypeById := function($byId) {
        (
            $dups := $getDuplicates($byId);
            $dupsFixed := $keys($dups){
                $: $bestFileById($lookup($dups, $))
            };
            $merge([$byId,$dupsFixed])
        )
    };    
    
    $fixTypeByName := function($byName) {
        (
            $dups := $getDuplicates($byName);
            $dupsFixed := $keys($dups){
                $: $bestFileByName($lookup($dups, $))
            };
            $merge([$byName,$dupsFixed])
        )
    };

    $fixType := function($typeObj) {
        (
            {
                'byUrl': $fixTypeByUrl($typeObj.byUrl),
                'byId': $fixTypeById($typeObj.byId),
                'byName': $fixTypeByName($typeObj.byName)
            }
        )
    };

    $fixVersion := function($versionObj) {
        (
            $types := $keys($versionObj);
            $types{
                $: $fixType($lookup($versionObj, $))
            }
        )
    };

    $versions := $keys($)[$not($ in ["packages", "files"])];

    $versions{
        'packages': $$.packages,
        'files': $$.files,
        $: $fixVersion($lookup($$, $))
    };
)`),
  extractCurrentPackagesFromIndex: jsonata('$keys(packages)'),
  checkPackagesMissingFromIndex: jsonata(`(
    $dirList := dirList.$replace('#', '@');
    
    $missingFromIndex := packages[$not($ in $dirList)];
    $missingFromCache := $dirList[$not($ in $$.packages)];

    [$append($missingFromIndex,$missingFromCache)];
  )`),
  isEmpty: jsonata(`(
    $_isEmpty := function($input) {(
      $exists($input) ? (
        $input in ['', null, {}, []] 
        or (
          $type($input) = 'string' 
          and $length($input) > 0 
          and $trim($input) = ''
        )
        ? true 
        : (
          $type($input) = 'object' 
            ? (
              $count(($keys($input).($lookup($input,$)).$not($_isEmpty($)))[$])=0
            )
            : $type($input) = 'array'
              ? (
                $count($input[$_isEmpty($)=false]) = 0
              ) 
              : false
        )
      ) : true
    )};
    $_isEmpty($value)
  )`),
  codeSystemToDictionary: jsonata('concept.**[$type($)="object"]{code:display}'),
  valueSetExpandDictionary: jsonata(`  (
    $expandInclude := function($include){(

      $exists($include.filter) ? undefined : (
        $vs := $include.valueSet.$valueSetExpand($);
        $concepts := (
          /*if*/ 
          $exists($include.concept) 
          /*then*/ ? 
          $include.(
            $sys := system;
            {
              system: concept{
                code: (
                  $exists(display) ? display : $lookup($codeSystemDictionary($sys),code)
                )
              }
            }
          ) 
          /*else*/ : (
            $exists($include.system) 
              ? $include.{
                system: $codeSystemDictionary($include.system)
              }
            ));
        $count($vs) > 0 ? (
          $exists($include.system) ? (
            $system = $include.system;
            {
              $system: $concepts.*.$sift(function($v,$k){(
                $exists(
                  $vs.$lookup($system) ~> $lookup($k)
                )
              )})
            }
          ) : (
            $vs
          )
        ) : $concepts
      )
    )};

    $compose := $vs.(   
      $countIncludes := $count(compose.include);
      $countExcludes := $count(compose.exclude);
    
      $includes := compose.include.$expandInclude($);
      $excludes := compose.exclude.$expandInclude($);
    
      $count($includes)=$countIncludes and $count($excludes)=$countExcludes ? (
        [$includes.(
          $currInclude := $;
          $systems := $keys($currInclude);
          $systems.(
            $system := $;
            $filteredDictionary := $lookup($currInclude, $system) ~> $sift(function($v,$k){(
              $not($exists($excludes.(
                $lookup($, $system) ~> $lookup($k)
              )))
            )});
            {$system: $filteredDictionary}
          )
        )]
      )
    );
    $count($compose) > 0 ? $compose;

  )`),
  testCodeAgainstVS: jsonata(`(
    $allCodes := $merge($vs.*);
    $lookup($allCodes, $value);
  )`),
  testCodingAgainstVS: jsonata(`(
    $system := $coding.system;
    $code := $coding.code;
    $exists($system) and $exists($code) ? (
      $codes := $merge($vs.$lookup($, $system));
      $lookup($codes, $code);
    )
  )`),
  testCodeableAgainstVS: jsonata(`(
    $codings := $codeable.$sift(function($v, $k){$substring($k,0,6) = 'coding'}).*;
    $codings.$testCodingAgainstVS($, $vs);
  )`)
};

export default expressions;
