<?php

namespace Ignite\Core\Console;

use DomainException;
use Exception;
use Ignite\Core\Contracts\Repositories\ImportRepository;
use Ignite\Core\Entities\Import;
use Ignite\Core\Events\ImportCompleted;
use Ignite\Core\Events\ImportStarted;
use Ignite\Core\Exceptions\ImportException;
use Ignite\Core\Models\Import\Importer;
use Illuminate\Console\Command;
use Illuminate\Database\QueryException;
use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Support\Facades\DB;
use League\Csv\Writer;
use Monolog\Logger;
use Symfony\Component\Console\Input\InputOption;
use Throwable;

class ImportCommand extends Command
{
    public const RESULT_COLUMN = 'result';

    /**
     * The console command name.
     *
     * @var string
     */
    protected $name = 'ignite:import';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Import data into Ignite from a CSV file.';

    /**
     * @var ImportRepository
     */
    protected $importRepository;

    /**
     * @var int
     */
    protected $timeStarted;

    /**
     * @var FilesystemManager
     */
    protected $filesystemManager;

    /**
     * Create a new command instance.
     *
     * @param ImportRepository $importRepository
     * @param FilesystemManager $filesystemManager
     */
    public function __construct(ImportRepository $importRepository, FilesystemManager $filesystemManager)
    {
        parent::__construct();

        $this->importRepository = $importRepository;
        $this->filesystemManager = $filesystemManager;
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     * @throws Exception
     */
    public function handle()
    {
        $id = (int)$this->option('id');
        $user = (int)$this->option('user');
        $dryRun = $this->isDryRun();

        $flagFile = md5($id) . ".flag";
        $disk = $this->filesystemManager->disk('local');
        $this->timeStarted = microtime(true);

        if (!$disk->exists($flagFile)) {
            $disk->put($flagFile, md5($flagFile));
        }

        try {
            $this->process($id, $user, $dryRun);
        } finally {
            $disk->delete($flagFile);
        }

        return 0;
    }

    /**
     * Process the import.
     *
     * @param int $id
     * @param int $user
     * @param bool $dryRun
     *
     * @return void
     * @throws Exception
     */
    protected function process($id, $user, $dryRun = false)
    {
        $import = $this->importRepository->find($id);

        $imported = 0;
        $rejected = 0;

        $log = $import->getLogger();

        event(new ImportStarted($import, $log));

        try {
            $importer = $import->resolveType();
            $this->info(sprintf('%s Importer', $import->getTypeLabel()));

            $msg = sprintf(
                'Running this process will import %s records. Are you sure you want to continue?',
                $importer->count()
            );
            if (!$this->option('no-interaction') && !$this->confirm($msg)) {
                return 0;
            }

            if (!$dryRun) {
                $import->update(['status' => Import::STATUS_PROCESSING]);
                $tempPath = tempnam(sys_get_temp_dir(), '');
                $writer = $this->getResultWriter($importer, $tempPath);
            }

            $importer->prepare();
        } catch (Throwable $e) {
            return $this->handleException($e, $log);
        }

        foreach ($importer->process() as $line => $data) {
            // if (0 == $line % 20) { echo "$line|"; }
            $importer->setResultColumn(null);
            $source = array_merge($data, [static::RESULT_COLUMN => Importer::RESULT_REJECTED]);

            DB::beginTransaction();
            try {
                $validator = $importer->validate($data);

                if ($validator->fails()) {
                    throw new DomainException(sprintf(
                        "Import record failed validation:\n%s",
                        json_encode($validator->errors()->toArray())
                    ));
                }

                $data = $importer->transform($data);
                $result = !$dryRun ? $importer->save($data) : $importer->drySave($data);

                if ($result) {
                    $source[static::RESULT_COLUMN] = $importer->getResultColumn() ?? Importer::RESULT_IMPORTED;
                }

                if ($result || !$importer->isErrorOnFailedSave()) {
                    $log->info($importer->formatImportMessage($line, $data), $data);
                    $imported++;
                } else {
                    throw new Exception('Failed to save record');
                }
                DB::commit();
            } catch (ImportException $e) {
                DB::rollBack();
                $message = $importer->formatRejectMessage($line, $data, $e);
                $log->error($message, $data);
                $rejected++;
            } catch (QueryException $e) {
                DB::rollBack();
                $message = $importer->formatRejectMessage($line, $data, $e->getPrevious());
                $log->error(preg_replace('/Error:\sSQLSTATE\[(.*)]:\s/', '', $message), $data);
                $rejected++;
            } catch (Throwable $e) {
                DB::rollBack();
                $message = $importer->formatRejectMessage($line, $data, $e);
                $log->error($message, $data);
                logger()->debug($e->getMessage(), [$data, $e]);  // full stack trace if needed
                $rejected++;
            }

            if (!$dryRun) {
                try {
                    $writer->insertOne($source);
                } catch (Throwable $e) {
                    $log->error($e->getMessage());
                }
            }

            gc_collect_cycles();
        }

        if ($dryRun) {
            $this->info(sprintf('%s records would be imported, %s would be rejected.', $imported, $rejected));
        } else {
            // Overwrite all the records in the original file when all records have been imported
            rename($tempPath, $import->getFilePath());

            $import->update([
                'run_by' => $user,
                'run_at' => now(),
                'imported' => $imported,
                'rejected' => $rejected,
                'status' => Import::STATUS_COMPLETE
            ]);

            $this->info(sprintf('%s records were imported, %s were rejected.', $imported, $rejected));
        }

        $this->postImport($importer, $import, $log);
    }

    /**
     * Determine if user asked for a dry run.
     *
     * @return bool
     */
    protected function isDryRun()
    {
        return (in_array($this->option('dry-run'), [
            true, 'true',
            1, '1',
            'yes', 'y',
            null
        ], true));
    }

    /**
     * Get the console command arguments.
     *
     * @return array
     */
    protected function getArguments()
    {
        return [];
    }

    /**
     * Get the console command options.
     *
     * @return array
     */
    protected function getOptions()
    {
        return [
            ['id', null, InputOption::VALUE_OPTIONAL, 'The ID of an existing import record.', null],
            ['dry-run', null, InputOption::VALUE_OPTIONAL, 'Should we run the import or dry-run it.', false],
            ['user', null, InputOption::VALUE_OPTIONAL, 'The user running the import.', 1]
        ];
    }

    /**
     * Starts the results writer.
     *
     * @param Importer $importer
     * @param string $tempPath
     * @return Writer
     */
    protected function getResultWriter(Importer $importer, string $tempPath): Writer
    {
        $writer = Writer::from($tempPath, 'w');

        $headers = $importer->getHeaders();
        if (!in_array(static::RESULT_COLUMN, $headers)) {
            $headers[] = static::RESULT_COLUMN;
        }
        $writer->insertOne($headers);

        return $writer;
    }

    /**
     * Starts the post import.
     *
     * @param Import $import
     * @param Logger $log
     */
    protected function postImport(Importer $importer, Import $import, Logger $log)
    {
        $timeStarted = $this->timeStarted;
        $timeEnded = microtime(true);
        $timeElapsed = $timeEnded - $this->timeStarted;
        $memoryGetUsage = memory_get_usage(true);
        $memoryGetPeakUsage = memory_get_peak_usage(true);
        $isDryRun = $this->isDryRun();

        $importer->postImport(compact(
            'import',
            'log',
            'timeStarted',
            'timeEnded',
            'timeElapsed',
            'memoryGetUsage',
            'memoryGetPeakUsage',
            'isDryRun'
        ));

        event(new ImportCompleted($import, $log));

        return $this;
    }

    /**
     * Handle an exception depending on the command verbosity.
     *
     * @param Throwable $e
     * @param Logger $log
     */
    protected function handleException(Throwable $e, Logger $log)
    {
        $error = sprintf('An error occurred: %s in %s on line %s', $e->getMessage(), $e->getFile(), $e->getLine());
        $log->error($error);
        $this->error($error);
    }
}
