#!/usr/bin/env perl
#
#   A software toolkit for the interconversion of standard data models for phenotypic data
#
#   This file is part of Convert::Pheno
#
#   Last Modified: Apr/09/2024
#
#   $VERSION taken from Convert::Pheno
#
#   Copyright (C) 2022-2026 Manuel Rueda - CNAG (manuel.rueda@cnag.eu)
#
#   License: Artistic License 2.0

package main;

use strict;
use warnings;
use autodie;
use feature      qw(say);
use Data::Dumper;
use Sys::Hostname;
use POSIX                          qw(strftime);
use File::Spec::Functions          qw(catdir catfile);
use File::ShareDir::ProjectDistDir qw(dist_dir);
use FindBin                        qw($Bin);
use lib "$Bin/../lib";
use Term::ANSIColor qw(:constants);
use Convert::Pheno;
use Convert::Pheno::CLI::Args qw(build_cli_request);
use Convert::Pheno::Execution::Files qw(execute_file_conversion);
use Convert::Pheno::IO::Atomic qw(
  write_atomically
);
use Convert::Pheno::Sink::FileSet qw(resolve_entity_output_file);

$Data::Dumper::Sortkeys = 1;

# Error decoration belongs to the CLI process, not to module consumers.
$SIG{__WARN__} = sub { warn "Warn: ", @_ };
$SIG{__DIE__}  = sub { die "Error: ", @_ };

# Defining a few variables
my $out_dir     = '.';
my $share_dir   = dist_dir('Convert-Pheno');
my $schema_file = catfile( $share_dir, 'schema', 'mapping-v2.json' );

sub print_help {
    my $out = shift || *STDOUT;
    print {$out} <<'EOF';
Usage:
  convert-pheno -i <input-type> <infile> -o <output-type> <outfile> [options]
  convert-pheno -ipxf <infile> -obff <outfile> [options]
  convert-pheno -ipxf <infile> -obff --entities <list> --out-dir <dir> [options]

Common input flags:
  -i <type> <file>    Generic input form
                      (pxf|bff|cbioportal|omop|redcap|cdisc-odm|dataset-json|dataset-xml|fhir|i2b2|pcornet|sentinel|csv|openehr)
  -ibff <file>      Beacon v2 Models ('individuals' JSON|YAML)
  -ipxf <file>      Phenopackets v2 (JSON|YAML)
  -icbioportal <path>
                    Experimental cBioPortal clinical study directory or ZIP
                    (input-only; BFF, PXF, or OMOP output)
  -iomop <inputs...>
                    OMOP-CDM CSV/TSV files, one directory or ZIP package,
                    or a PostgreSQL dump
  -iopenehr <files...>
                    Experimental openEHR canonical JSON/YAML compositions
                    (currently EHRbase-oriented; input-only; BFF/PXF output)
  -iredcap <file>   REDCap export CSV
  -icdisc-odm <file>
                    CDISC-ODM Snapshot XML (1.3.1, 1.3.2, or 2.0)
  -idataset-json <files...>
                    Experimental CDISC Dataset-JSON v1.1 SDTM datasets
                    (input-only; loaded in memory; BFF, PXF, or OMOP output)
  -idataset-xml <files...>
                    Experimental CDISC Dataset-XML v1.0 SDTM datasets
                    (requires --define-xml; loaded in memory)
  -ifhir <files...>  Experimental FHIR R4 JSON Bundles, including mCODE
                    (input-only; loaded in memory; BFF, PXF, or OMOP output)
  -ii2b2 <paths...>  Experimental i2b2 table exports
  -ipcornet <paths...>
                    Experimental PCORnet CDM table exports
  -isentinel <paths...>
                    Experimental Sentinel CDM table exports
                    Each accepts delimited files, one directory, or one ZIP;
                    input-only and loaded in memory for BFF, PXF, or OMOP output
  -icsv <file>      Raw CSV

Common output flags:
  -o <type> <file>    Generic output form (bff|pxf|omop|csv|jsonf|jsonld)
  -obff [file]      Beacon-oriented JSON/YAML output
                    Use -obff FILE for individuals-only BFF output
                    Use -obff with --entities --out-dir for entity-aware BFF
  -opxf <file>      Phenopackets v2 output
  -oomop            OMOP-CDM CSV table output (use with --out-dir)
  -ocsv <file>      Flattened CSV (with -ibff or -ipxf)
  -ojsonf <file>    Flattened 1D JSON/YAML (with -ibff or -ipxf)
  -ojsonld <file>   JSON-LD / YAML-LD (with -ibff or -ipxf)

Output behavior:
  --out-dir <dir>       Output directory (required for entity-aware BFF mode)
  --entities <list>     Beacon entities for BFF output [individuals]
                        Supported: individuals, biosamples, datasets, cohorts
                        biosamples are emitted from -ipxf, cBioPortal samples,
                        FHIR Specimen, OMOP SPECIMEN, or mapping rules
                        datasets and cohorts are synthesized from individuals
                        Use with -obff and --out-dir
  --out-name k=file     Override one multi-file output name; repeat as needed
                        Use entity keys for BFF and table keys for OMOP
  -O                    Overwrite output file(s)

Mapping and search:
  --mapping-file <file>           Mapping V2 YAML or JSON file targeting
                                  Beacon schema 2.0.0, or optional terminology
                                  correspondences for BFF-to-OMOP CDM 5.4
                                  Optional for cBioPortal clinical packages
                                  and CDISC Dataset terminology enrichment
                                  Optional compact dataset/cohort metadata for
                                  built-in BFF input routes
                                  ODM uses source.profile redcap for REDCap
                                  exports, or cdisc-odm for embedded metadata
  --redcap-dictionary|-rcd <file> REDCap data dictionary CSV file; required
                                  for REDCap CSV and REDCap-origin ODM only
  --define-xml <file>             Define-XML v2.0/v2.1 metadata required by
                                  Dataset-XML and optional for Dataset-JSON
  --schema-file <file>            Alternative JSON Schema for mapping files
  --self-validate-schema|-svs     Self-validate the mapping JSON Schema
                                  (mainly for author/development checks)
  --search <type>                 Ontology label search mode [exact|mixed|fuzzy]
                                  Default: exact
  --text-similarity-method <m>    Token similarity for mixed/fuzzy search
                                  [cosine|dice], default: cosine
  --min-text-similarity-score <s> Minimum score for mixed/fuzzy search
                                  Default: 0.8
  --levenshtein-weight <w>        Levenshtein weight in fuzzy composite score
                                  Default: 0.1, range: 0-1
  --term-audit <file>             Write terminology decisions and candidate
                                  scores [.tsv|.tsv.gz|.xlsx]
  --source-info|--no-source-info  Preserve or omit raw source provenance in BFF
                                  output [source-info]
  --include-dataset-id            Copy beacon.datasets.defaults.id to top-level
                                  datasetId in individuals and biosamples
                                  (backend compatibility extension; off by default)

OMOP:
  --ohdsi-db                      Enable Athena-OHDSI lookup
  --path-to-ohdsi-db <dir>        Directory containing <ohdsi.db>
  --omop-tables <list>            Restrict OMOP tables
  --exposures-file <file>         OMOP concept_id list treated as exposures
  --stream|--no-stream            Incremental OMOP processing with single-file
                                  line-delimited JSON output [no-stream]
                                  Supports individuals-only <-obff FILE>, or
                                  entity-aware <-obff --entities individuals
                                  biosamples --out-dir DIR>
  --sql2csv                       Print SQL tables instead of converting
  --max-lines-sql <n>             OMOP processing cap: rows per SQL table and
                                  individuals in non-stream mode [500]

Other useful options:
  --default-vital-status <s>   Fallback PXF subject vitalStatus
                               [ALIVE|DECEASED|UNKNOWN_STATUS]
                               Used only with PXF output when no source
                               vitalStatus is available
  --separator|--sep <char>        Delimiter for CSV input [; for .csv]
  --username|-u <name>            Override username stored in metadata
  --test                          Skip time-varying metadata for stable tests
  --log [file]                    Write resolved request JSON log
                                  [convert-pheno-log.json]
  --color|--no-color              Enable or disable colored terminal output
                                  [color]
  --debug <level>                 Print internal request/debug data
                                  (level >= 2 also prints DB lookup summary)
  --verbose|-v                    Print progress information
  --version|-V
  --help
  --man

Examples:
  convert-pheno -i pxf pxf.json -o bff individuals.json
  convert-pheno -ipxf pxf.json -obff individuals.json
  convert-pheno -ipxf pxf.json -obff --entities biosamples --out-dir out/
  convert-pheno -ipxf pxf.json -obff --entities individuals biosamples --out-dir out/ --out-name biosamples=samples.json
  convert-pheno -ipxf pxf.json -obff --entities individuals datasets cohorts --out-dir out/
  convert-pheno -icbioportal study/ -obff --entities individuals biosamples datasets cohorts --out-dir out/
  convert-pheno -i cbioportal study.zip -o pxf phenopackets.json
  convert-pheno -icsv data.csv --mapping-file mapping.yaml -obff --entities individuals datasets cohorts --out-dir out/
  convert-pheno -iredcap data.csv -rcd dictionary.csv --mapping-file mapping.yaml -obff --entities individuals biosamples --out-dir out/
  convert-pheno -icdisc-odm study.xml --mapping-file odm-mapping.yaml -obff individuals.json
  convert-pheno -icdisc-odm redcap.xml -rcd dictionary.csv --mapping-file redcap-mapping.yaml -obff individuals.json
  convert-pheno -idataset-json dm.json mh.json lb.json --mapping-file sdtm-terms.yaml -obff individuals.json
  convert-pheno -i dataset-json dm.json mh.json lb.json -o pxf phenopackets.json
  convert-pheno -idataset-json dm.json mh.json lb.json -oomop --out-dir omop_out/ --ohdsi-db
  convert-pheno -idataset-xml dm.xml mh.xml lb.xml --define-xml define.xml -obff individuals.json
  convert-pheno -i dataset-xml dm.xml mh.xml lb.xml --define-xml define.xml -o pxf phenopackets.json
  convert-pheno -ifhir bundle.json -obff individuals.json
  convert-pheno -ifhir bundle.json -obff --entities individuals biosamples --out-dir out/
  convert-pheno -i fhir bundle.json -o pxf phenopacket.json
  convert-pheno -ii2b2 i2b2-export/ -obff individuals.json
  convert-pheno -ipcornet pcornet-tables.zip -opxf phenopackets.json
  convert-pheno -i sentinel sentinel-export/ -o omop --out-dir omop_out/ --ohdsi-db
  convert-pheno -iomop PERSON.csv CONCEPT.csv SPECIMEN.csv -obff --entities biosamples --out-dir out/
  convert-pheno -ibff individuals.json -oomop --out-dir omop_out/ --ohdsi-db
  convert-pheno -ibff individuals.json -oomop --out-dir omop_out/ --out-name PERSON=patients.csv --ohdsi-db
  convert-pheno -iomop dump.sql.gz -opxf phenopackets.json.gz --default-vital-status UNKNOWN_STATUS --ohdsi-db
  convert-pheno -iomop dump.sql.gz -obff individuals.json.gz --stream --ohdsi-db
  convert-pheno -iomop dump.sql.gz -obff --stream --entities individuals biosamples --out-dir out/ --ohdsi-db
  convert-pheno -i openehr patient-set.json -o bff individual.json
  convert-pheno -i openehr patient-set.json -o pxf phenopacket.json

BFF output modes:
  -obff FILE keeps the individuals-only BFF behavior.
  -obff --entities ... --out-dir DIR writes one file per requested BFF entity.
  -oomop --out-dir DIR writes one file per emitted OMOP table.
  If a PXF input also contains biosamples, convert-pheno warns and preserves them
  under info.phenopacket.biosamples unless --entities is used.
  PXF output still defaults subject.vitalStatus to ALIVE unless a preserved
  source vitalStatus or --default-vital-status is available.
  openEHR input support is currently experimental and limited to BFF or PXF output.
  Dataset-JSON input support is experimental, SDTM-oriented, and processed in memory.
  Dataset-XML input support is experimental, SDTM-oriented, requires Define-XML,
  and is processed in memory.
  FHIR input support is experimental, targets R4 JSON Bundles including mCODE,
  and is processed in memory.
  i2b2, PCORnet CDM, and Sentinel CDM input support is experimental, reads
  exported tables rather than live databases, and is processed in memory.

Docs:
  Site:  https://cnag-biomedical-informatics.github.io/convert-pheno
  CLI:   https://cnag-biomedical-informatics.github.io/convert-pheno/use-as-a-command-line-interface/
  Install:
         https://cnag-biomedical-informatics.github.io/convert-pheno/download-and-installation/
EOF
}

sub print_usage_summary {
    my $out = shift || *STDERR;
    print {$out} <<'EOF';
Usage:
  convert-pheno -i <input-type> <infile> -o <output-type> <outfile> [options]
  convert-pheno -ipxf <infile> -obff <outfile> [options]
  convert-pheno -ipxf <infile> -obff --entities <list> --out-dir <dir> [options]

Run `convert-pheno --help` for full usage
EOF
}

sub usage_error {
    my ($message) = @_;
    print STDERR "Error: $message";
    print STDERR "\n" unless $message =~ /\n\z/;
    print STDERR "\n";
    print_usage_summary(*STDERR);
    exit 1;
}

sub print_man {
    print <<'EOF';
Full CLI documentation now lives in Markdown instead of POD.

Documentation site:
  https://cnag-biomedical-informatics.github.io/convert-pheno

CLI guide:
  https://cnag-biomedical-informatics.github.io/convert-pheno/use-as-a-command-line-interface/

Installation:
  https://cnag-biomedical-informatics.github.io/convert-pheno/download-and-installation/
EOF
}

my $cli = build_cli_request(
    argv        => \@ARGV,
    usage_error => \&usage_error,
    schema_file => $schema_file,
    out_dir     => $out_dir,
    color       => 1,
    stream      => 0,
    ohdsi_db    => 0,
);

if ( $cli->{action} eq 'help' ) { print_help(); exit 0; }
if ( $cli->{action} eq 'man' )  { print_man();  exit 0; }
if ( $cli->{action} eq 'version' ) { say "$0 Version $VERSION"; exit 0; }

my $data      = $cli->{data};
my $out_file  = $cli->{out_file};
my $log_file  = $cli->{log_file};
my $overwrite = $cli->{overwrite};
my $verbose   = $cli->{verbose};
my $stream    = $cli->{stream};
my $color     = $cli->{color};
my $log       = $data->{log};

$ENV{'ANSI_COLORS_DISABLED'} = 1 unless $color;

print Dumper $data if defined $data->{debug};

# Start printing to STDOUT
say BOLD CYAN program_header($VERSION), RESET if $verbose;

# Save log file if $log (before $data gets blessed)
write_log( $log_file, $data ) if defined $log;

#############################
# START DATA TRANSFORMATION #
#############################

convert( $out_file, $data );

###########################
# END DATA TRANSFORMATION #
###########################

sub convert {
    my ( $o_file, $l_data ) = @_;

    my $entity_mode =
      $l_data->{method} =~ /2bff$/
      && (
        @{ $l_data->{entities} || [] } != 1
        || ( $l_data->{entities}[0] || 'individuals' ) ne 'individuals'
      );

    maybe_warn_single_file_pxf_biosamples( $l_data, $entity_mode );
    prepare_output_targets( $l_data, $o_file, $entity_mode );

    # Start verbose
    print BOLD BLUE program_body($l_data), RESET if $verbose;

    # Creating object
    my $convert = Convert::Pheno->new($l_data);
    execute_file_conversion($convert, { %$l_data, out_file => $o_file },
        before_write => sub { ask_overwrite($_[0]) unless $overwrite; return 1 },
        on_write => sub { say BOLD GREEN "Writing <$_[0]> file\n", RESET if $verbose; return 1 },
    );

    # Finish
    print BOLD GREEN program_footer(), RESET if $verbose;
}

sub maybe_warn_single_file_pxf_biosamples {
    my ( $l_data, $entity_mode ) = @_;

    return if $entity_mode;
    return unless $l_data->{method} eq 'pxf2bff';
    return unless defined $l_data->{in_file};

    my $input = io_yaml_or_json(
        {
            filepath => $l_data->{in_file},
            mode     => 'read',
        }
    );

    my @items = ref($input) eq 'ARRAY' ? @$input : ($input);

    for my $item (@items) {
        next unless ref($item) eq 'HASH';
        my $phenopacket =
          exists $item->{phenopacket} ? $item->{phenopacket} : $item;
        next unless ref($phenopacket) eq 'HASH';
        next
          unless exists $phenopacket->{biosamples}
          && ref( $phenopacket->{biosamples} ) eq 'ARRAY'
          && @{ $phenopacket->{biosamples} };

        print STDERR <<'EOF';
Warning: input PXF contains biosamples. Single-file <-obff FILE> mode emits only <individuals>; biosamples were preserved under <info.phenopacket.biosamples>. Use <-obff --entities biosamples --out-dir DIR> or <-obff --entities individuals biosamples --out-dir DIR> for first-class biosample output.
EOF
        return;
    }

    return;
}

sub prepare_output_targets {
    my ( $l_data, $o_file, $entity_mode ) = @_;

    if ($entity_mode) {
        for my $entity ( @{ $l_data->{entities} } ) {
            my $entity_file = resolve_entity_output_file( $l_data, $entity );
            ask_overwrite($entity_file) unless $overwrite;
        }
        return;
    }

    return if $l_data->{method} =~ /2omop$/;

    ask_overwrite($o_file) unless $overwrite;
}

sub program_header {
    my $version = shift;
    my $str     = <<EOF;
****************************************
*  Phenotypic Data Model Convert Tool  *
*          - CONVERT-PHENO -           *
*            Version: $version             *
*   (C) 2022-2026 Manuel Rueda, CNAG   *
*       The Artistic License 2.0       *
****************************************
EOF
    return $str;
}

sub program_footer {
    my $str = <<EOF;
All done!

EOF
    return $str;
}

sub program_body {
    my $l_data = shift;
    my $file =
      @{ $l_data->{in_files} || [] }
      ? join ',', @{ $l_data->{in_files} }
      : $l_data->{in_file};
    my $msg = <<EOF;
==== METHOD: <$l_data->{method}> ====
Processing: <$file>
EOF
    return $msg;
}

sub write_log {
    my ( $log, $data ) = @_;

    # Detecting the number of logical CPUs across different OSes
    my $os = $^O;
    chomp(
        my $threadshost =
          lc($os) eq 'darwin' ? qx{/usr/sbin/sysctl -n hw.logicalcpu}
        : lc($os) eq 'freebsd' ? qx{sysctl -n hw.ncpu}
        : $os eq 'MSWin32'     ? qx{wmic cpu get NumberOfLogicalProcessors}
        :                        qx{/usr/bin/nproc} // 1
    );

    # For the Windows command, the result will also contain the string
    # "NumberOfLogicalProcessors" which is the header of the output.
    # So we need to extract the actual number from it:
    if ( $os eq 'MSWin32' ) {
        ($threadshost) = $threadshost =~ /(\d+)/;
    }
    $threadshost = 0 + $threadshost;    # coercing it to be a number

    my $info = {
        date        => ( strftime "%a %b %e %H:%M:%S %Y", localtime ),
        threadshost => $threadshost,
        hostname    => hostname,
        id          => $data->{id},                                      # string
        version     => $VERSION,
             user   => $ENV{'LOGNAME'}
          || $ENV{'USER'}
          || $ENV{'USERNAME'}
          || 'dummy-user'
    };

    # Saving file
    say BOLD GREEN "Writing <$log> file\n" if $verbose;
    write_atomically(
        $log,
        sub {
            my ($staged) = @_;
            io_yaml_or_json(
                {
                    filepath => $staged,
                    mode     => 'write',
                    data     => { info => $info, data => $data }
                }
            );
        }
    );
}

sub ask_overwrite {
    my $filepath = shift;

    # Check if the file exists
    if ( -f $filepath ) {
        my $attempt_count = 0;
        my $max_attempts  = 5;

        while ( $attempt_count < $max_attempts ) {
            print BOLD RED "<$filepath> exists. Overwrite? [Y/n]: ";
            my $input = <STDIN>;
            chomp $input;

            if ( $input eq 'Y' ) {
                print RESET;
                return;    # Exit after successful deletion
            }
            elsif ( $input eq 'n' ) {
                die "Operation aborted by the user.\n";
            }
            else {
                say "Invalid input. Please enter 'Y' or 'n'.";
                $attempt_count++;
            }
        }
        die "Too many invalid attempts. Operation aborted.\n";
    }
}
