<?php

namespace App\Models;

use App\Jobs\ProjectDownloadRDSJob;
use App\Jobs\ProjectDownloadTiffJob;
use App\Jobs\ProjectInterpolateGrowthModelJob;
use App\Jobs\ProjectMosiacGeneratorJob;
use App\Jobs\ProjectReportGeneratorJob;
use Carbon\CarbonPeriod;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Facades\Excel;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate;

class Project extends Model
{
    use HasFactory;

    protected $casts = [
        'min_harvest_date' => 'date'
    ];

    protected $fillable = [
        'name',
        'mail_template',
        'mail_subject',
        'mail_frequency',
        'mail_day',
        'mail_scheduled',
        'download_path',
        'pivot_json_path',
        'span_json_path',
        'harvest_json_path',
        'min_harvest_date',
        'borders',
    ];

    public static function saveWithFormData(mixed $formData)
    {
        $uniqueIdentifier = ['id' => $formData['id'] ?? null];
        /**
         * @var Project $project
         */
        logger($formData);
        $project = Project::updateOrCreate($uniqueIdentifier, $formData);
        $baseFrom = 'livewire-tmp/';
        $baseTo = $project->download_path.'/Data/';
        if ($formData['pivot_file']) {
            Storage::copy($baseFrom.$formData['pivot_file']['tmpFilename'], $baseTo.'pivot.geojson');
            $project->update(['pivot_json_path' => $baseTo.'pivot.geojson']);
        }
        if ($formData['span_file']) {
            Storage::copy($baseFrom.$formData['span_file']['tmpFilename'], $baseTo.'span.geojson');
            $project->update(['span_json_path' => $baseTo.'span.geojson']);
        }
        if ($formData['harvest_file']) {
            Storage::copy($baseFrom.$formData['harvest_file']['tmpFilename'],
                $baseTo.'harvest.'.$formData['harvest_file']['extension']);
            if ($project->update(['harvest_json_path' => $baseTo.'harvest.'.$formData['harvest_file']['extension']])) {
                $project->setMinHarvestDate();
            }
        }
        self::upsertMailRecipients($formData, $project);
    }

    private static function upsertMailRecipients($formData, Project $project)
    {
        $mailRecipientsData = array_map(function ($mailRecipient) use ($project) {
            $mailRecipient['project_id'] = $project->id;
            unset($mailRecipient['created_at']);
            unset($mailRecipient['updated_at']);
            $mailRecipient['id'] ??= null;
            return $mailRecipient;
        }, $formData['mail_recipients'] ?? []);
        ProjectEmailRecipient::upsert(
            $mailRecipientsData,
            ['id', 'project_id'],
            ['name', 'email',]
        );
    }

    public function getMosaicPath()
    {
        return sprintf('%s/%s', $this->download_path, 'weekly_mosaic');
    }

    public function getMosaicList(): Collection
    {
        return collect(Storage::files($this->getMosaicPath()))
            ->filter(fn($filename) => Str::endsWith($filename, '.tif'))
            ->values();
    }

    protected static function boot()
    {
        parent::boot(); // TODO: Change the autogenerated stub

        static::deleting(function ($project) {
            $project->emailRecipients()->delete();
            $project->mailings()->each(function ($mailing) {
                $mailing->attachments()->delete();
                $mailing->recipients()->delete();
            });
            $project->mailings()->delete();
        });
    }

    public function reports()
    {
        return $this->hasMany(ProjectReport::class);
    }

    public function getAttachmentPathAttribute()
    {
        return storage_path(sprintf('%s/attachments', $this->download_path));

        return '/storage/'.$this->download_path.'/attachments';
    }

    public function emailRecipients()
    {
        return $this->hasMany(ProjectEmailRecipient::class);
    }

    public function mailings()
    {
        return $this->hasMany(ProjectMailing::class);
    }

    public function downloads(): \Illuminate\Database\Eloquent\Relations\HasMany
    {
        return $this->hasMany(ProjectDownload::class);
    }

    public function nonFailedDownloads(): \Illuminate\Database\Eloquent\Relations\HasMany
    {
        return $this->hasMany(ProjectDownload::class)->where('status', '<>', 'failed');
    }

    public function mosaics(): \Illuminate\Database\Eloquent\Relations\HasMany
    {
        return $this->hasMany(ProjectMosaic::class);
    }


    public function allMosaicsPresent(Carbon $endDate): bool
    {
        // end date is in the future
        if ($endDate->isFuture()) {
            throw new \Exception('Mosaic can\'t be generated for the future. Change the end date.');
        }
        $mosaicsNotPresentInFilesystem = $this->getMissingMosaicsInFileSystem($endDate);
        if ($mosaicsNotPresentInFilesystem->count() === 0) {
            return true;
        }
        $message = sprintf(
            'Missing mosaics: %s',
            $mosaicsNotPresentInFilesystem->implode(', ')
        );
        throw new \Exception($message);
    }

    public function getMissingMosaicsInFileSystem(Carbon $endDate)
    {
        return collect($this->getMosaicFilenameListByEndDate($endDate))
            ->filter(function ($filename) {
                return !$this->getMosaicList()->contains(function ($mosaicFilename) use ($filename) {
                    return Str::endsWith($mosaicFilename, substr($filename, -16));
                });
            });
    }

    public static function getMosaicFilenameListByEndDate(Carbon $endDate, int $offset = 7): \Generator
    {
        for ($i = 0; $i < 4; $i++) {
            yield (ProjectMosaic::getFilenameByPeriod($endDate->copy()->subDays($offset * $i), $offset));
        }
    }

    public function getMosiacList()
    {
        return collect(Storage::files($this->getMosaicPath()))
            ->filter(fn($file) => Str::endsWith($file, '.tif'))
            ->sortByDesc(function ($file) {
                $parts = explode('_', str_replace('.tif', '', $file));
                $week = $parts[1];
                $year = $parts[2];
                return $year.sprintf('%02d', $week);
            })
            ->values();
    }

    public static function getAllDatesOfWeeksInYear($year, $weekNumber): Collection
    {
        $startOfWeek = Carbon::now()->setISODate($year, $weekNumber)->startOfWeek();
        $dates = collect([]);

        for ($day = 0; $day < 7; $day++) {
            $dates->push((clone $startOfWeek)->addDays($day)->toDateString());
        }

        return $dates;
    }

    public function allMergedTiffsPresent(Collection $haystack, Collection $needles)
    {
        $needlesNotInHaystack = $needles->filter(function ($needle) use ($haystack) {
            return !$haystack->contains(function ($item) use ($needle) {
                return str_contains($item, $needle);
            });
        });

        if ($needlesNotInHaystack->count() > 0) {
            $message = sprintf(
                'Missing merged tiffs: %s',
                $needlesNotInHaystack->implode(', ')
            );

            throw new \Exception($message);
        }
        return true;
    }

    public function getMergedTiffList()
    {
        return collect(Storage::files($this->download_path.'/merged_final_tif'))
            ->filter(fn($file) => Str::endsWith($file, '.tif'))
            ->sortByDesc(function ($file) {
                $parts = explode('_', str_replace('.tif', '', $file));
                $date = $parts[1];
                return $date;
            })
            ->values();
    }

    public function hasPendingDownload(): bool
    {
        return $this->downloads()->statusPending()->count() > 0;
    }

    public function hasPendingReport(): bool
    {
        return $this->reports()->statusPending()->count() > 0;
    }

    public function hasPendingMosaic(): bool
    {
        return $this->mosaics()->statusPending()->count() > 0;
    }

    public function startDownload(Carbon $date)
    {
        $downloadRequest = $this->downloads()->updateOrCreate(
            [
                'project_id' => $this->id, // of een andere manier om project_id te bepalen
                'name'       => sprintf('%s.tif', $date->format('Y-m-d')),
            ],
            [
                'path' => sprintf('%s/%s/%s.tif', $this->download_path, 'merged_final_tif', $date->format('Y-m-d')),
            ]
        );
        ProjectDownloadTiffJob::dispatch($downloadRequest, $date);
    }

    public function schedule()
    {
        //TODO check the ranges.
        $this->scheduleReport();
    }

    public function shouldSchedule(): bool
    {
        if (strtolower($this->mail_day) === strtolower(now()->englishDayOfWeek)) {
            return strtolower($this->mail_frequency) === 'weekly' || now()->day <= 7;
        }
        return false;
    }


    public function hasInvalidMosaicFor(Carbon $endDate, int $offset): bool
    {
        // parameters : $
        // check if the mail day happens the day before mosaic -> good
        $dayOfWeekIso = Carbon::parse($this->mail_day)->dayOfWeekIso;
        $min_updated_at_date = $endDate->copy()
            ->startOfWeek()
            ->addDays($dayOfWeekIso - 1)
            ->format('Y-m-d');

        return $this->mosaics()
            ->where('updated_at', '>=', $min_updated_at_date)
            ->statusSuccess()
            ->where(['end_date' => $endDate, 'offset' => $offset])
            ->exists();

    }

    public function scheduleReport(?Carbon $endDate = null, ?int $offset = null)
    {
        if ($endDate?->isFuture() || $endDate?->isToday() || $offset <= 0) {
            logger('EndDate is today or in the future.');
            $endDate = null;
            $offset = null;
        }

        $endDate ??= Carbon::yesterday();
        $offset ??= 7;

        for ($step = 0; $step<=3; $step++) {
            $latestMosaicToDelete = ProjectMosaic::projectMosaicNameFormat($endDate->clone(), $offset*$step);
            logger('Deleting mosaic: '.$latestMosaicToDelete);
            $this->mosaics()->where('name', $latestMosaicToDelete)->first()?->delete();
        }

        logger('Scheduling report for '.$endDate->format('d-m-Y').' with offset '.$offset.' days');
        Bus::chain([
            Bus::batch($this->getFileDownloadsFor($endDate->clone(), $offset)),
            Bus::batch($this->getMosaicsFor($endDate->clone(), $offset)),
            Bus::batch(
                [
                    new ProjectInterpolateGrowthModelJob($this),
                    $this->getReportFor($endDate->clone(), $offset, true)
                ]),
        ])
            ->dispatch();
        return "done";
    }

    public function scheduleTestReport()
    {
        $endDate = Carbon::yesterday();
        $offset = 7;
        $this->mail_day = $endDate->dayName;

        for ($step = 0; $step<=3; $step++) {
            $latestMosaicToDelete = ProjectMosaic::projectMosaicNameFormat($endDate->clone(), $offset*$step);
            logger('Deleting mosaic: '.$latestMosaicToDelete);
            $this->mosaics()->where('name', $latestMosaicToDelete)->first()?->delete();
        }

        logger('Scheduling test report for '.$endDate->format('d-m-Y').' with offset '.$offset.' days');
        Bus::chain([
            Bus::batch($this->getFileDownloadsFor($endDate->clone(), $offset)),
            Bus::batch($this->getTestMosaicsFor($endDate->clone())),
            Bus::batch(
                [
                    new ProjectInterpolateGrowthModelJob($this),
                    $this->getTestReportFor($endDate->clone(), $offset, true)
                ]),
        ])
            ->dispatch();
        return "done";
    }

    public function getReportFor(Carbon $endDate, int $offset = 7, $sendMail = false): ProjectReportGeneratorJob
    {
        $report = $this->reports()->create([
            'name'     => 'Report of the '.$endDate->format('d-m-Y').' from the past '.$offset.' days',
            'end_date' => $endDate,
            'offset'   => $offset,
            'path'     => 'reports/'.ProjectReport::getFileName($endDate, $offset).'.docx',
        ]);

        return (new ProjectReportGeneratorJob($report, $sendMail));
    }

    public function getTestReportFor(Carbon $endDate, int $offset = 7, $sendMail = false): ProjectReportGeneratorJob
    {
        $report = $this->reports()->create([
            'name'     => 'Test Report of the '.$endDate->format('d-m-Y').' from the past '.$offset.' days',
            'end_date' => $endDate,
            'offset'   => $offset,
            'path'     => 'reports/'.ProjectReport::getFileName($endDate, $offset).'.docx',
        ]);

        return (new ProjectReportGeneratorJob($report, $sendMail, true));
    }

    public function getFileDownloadsFor(Carbon $endDate, int $offset = 7): array
    {
        $startOfRange = (clone $endDate)->subdays(4 * $offset - 1);
        $dateRange = CarbonPeriod::create($startOfRange, $endDate);

        return collect($dateRange)
            ->map(fn($date) => ProjectDownloadTiffJob::handleForDate($this, $date))
            ->filter()
            ->toArray();
    }

    public function getMosaicsFor(Carbon $endDate, int $offset = 7): array
    {
        return collect(range(0, 3))
            ->map(function () use ($endDate, $offset) {
                $currentEndDate = $endDate->clone();
                if (!$currentEndDate->isDayOfWeek($this->mail_day)) {
                    $endDate->previous($this->mail_day);
                }
                $endDate->subDay();
                $calculatedOffSet = (int) $endDate->clone()->diffInDays($currentEndDate);

                return ProjectMosiacGeneratorJob::handleFor($this, $currentEndDate, $calculatedOffSet);
            })
            ->filter()
            ->toArray();
    }

    public function getTestMosaicsFor(Carbon $endDate)
    {
        return collect(range(0, 3))
            ->map(function () use ($endDate) {
                $currentEndDate = $endDate->clone();
                $endDate->subWeek();

                return ProjectMosiacGeneratorJob::handleFor($this, $currentEndDate, 7);
            })
            ->filter()
            ->toArray();
    }

    public function handleMissingDownloads()
    {
        $this->getMissingDownloads()
            ->each(function (Carbon $date) {
                dispatch(ProjectDownloadTiffJob::handleForDate($this, $date));
            });
    }

    public function hasMissingDownloadsDateInHarvestFile(): bool
    {
        return $this->getMissingDownloads()->count() > 0;
    }

    public function getMissingDownloads(): Collection
    {
        if (!$this->min_harvest_date) {
            return collect([]);
        }
        $harvest_dates = $this->nonFailedDownloads()->get()->map(fn($d) => $d->date);
        $all_dates = collect(CarbonPeriod::create($this->min_harvest_date, '1 day', now())->toArray());
        return $all_dates->diff($harvest_dates);
    }

    public function setMinHarvestDate(): bool
    {
        if (!$this->harvest_json_path) {
            return false;
        }
        return $this->update(['min_harvest_date' => $this->getMinimumDateFromHarvestExcelFile()->format('Y-m-d')]);
    }

    private function getMinimumDateFromHarvestExcelFile(): Carbon
    {
        $value = Storage::disk('local')->path($this->harvest_json_path);
        $data = Excel::toCollection(new \App\Imports\ExcelFileImport(), $value);
        $season_start_index = $data->first()->first()->search('season_start');
        return collect($data->first()->slice(1))->reduce(function ($carry, $value, $key) use ($season_start_index) {
            return min($carry, Carbon::instance(SharedDate::excelToDateTimeObject($value[$season_start_index])));
        }, now());
    }

    public function newDownloadsUploaded()
    {
//        $this->createDownloadRecordsInDatabaseAndUpdateStatusForAllDownloadedImages();
        $date = Carbon::parse('2023-02-09');
        $now = Carbon::now();

        $offset = (int) $date->diffInDays($now);
        dispatch_sync(new ProjectDownloadRDSJob($this, Carbon::yesterday(), $offset));
        dispatch_sync(new ProjectInterpolateGrowthModelJob($this));
    }

    public function getRdsAsDownload()
    {
        $path = $this->download_path.'/Data/extracted_ci/cumulative_vals/combined_CI_data.rds';

        return Storage::download(
            $path, 'combined_CI_data.rds'
        );
    }

    public function getTifsAsZip(Carbon $startDate, Carbon $endDate)
    {
        $path = $this->download_path.'/merged_final_tif';
        $files = collect(Storage::files($path))
            ->filter(fn($file) => Str::endsWith($file, '.tif'))
            ->filter(function ($file) use ($startDate, $endDate) {
                $dateString = str_replace('.tif', '', basename($file));
                $date = Carbon::parse($dateString);
                return $date->between($startDate, $endDate);
            });
        logger(__CLASS__.'::'.__METHOD__.'::'.__LINE__);
        logger($files);

        return $this->createZipArchiveAndReturn($files);
    }

    public function getMosaicsAsZip(Carbon $startDate, Carbon $endDate)
    {
        $path = $this->download_path.'/weekly_mosaic';

        // create a collection of all week numbers and years for given date range
        $allowedFiles = collect(CarbonPeriod::create($startDate, $endDate)->toArray())
            ->map(fn($date) => sprintf('week_%s_%s.tif', $date->weekOfYear, $date->year))
            ->unique();


        $files = collect(Storage::files($path))
            ->filter(fn($file) => Str::endsWith($file, '.tif'))
            ->filter(function ($file) use ($allowedFiles) {
                return $allowedFiles->contains(basename($file));
            });
        putenv('TMPDIR='.storage_path('app'));

        return $this->createZipArchiveAndReturn($files);
    }

    private function createZipArchiveAndReturn(Collection $files)
    {
        $zipPath = storage_path('app/'.$this->download_path.'/download.zip');
        $zip = new \ZipArchive();
        $zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
        $files->each(function ($file) use ($zip) {
            $zip->addFile(storage_path('app/'.$file), basename($file));
        });
        $zip->close();

        return response()->download(
            $zipPath, 'download.zip'
        );
    }

    public function createDownloadRecordsInDatabaseAndUpdateStatusForAllDownloadedImages(): void
    {
        $merged_tiffs = $this->getMergedTiffList();
        $this->downloads->each(function ($download) use ($merged_tiffs) {
            if ($merged_tiffs->contains($download->path) && $download->status !== 'success') {
                $download->setStatusSuccess();
            }
        });

        $merged_tiffs->each(function ($path) {
            if ($this->downloads()->where('path', $path)->count() === 0) {
                $this->downloads()->create([
                    'path'   => $path,
                    'status' => 'success',
                    'name'   => explode('/', $path)[count(explode('/', $path)) - 1]
                ]);
            }
        });
    }

    public function createAllMosaicsInDatabaseAndUpdateStatusForAllDownloadedImages()
    {
        $list_of_desired_mosaics = $this->getMergedTiffList()
            ->map(fn($file) => str_replace('.tif', '', basename($file)))
            ->map(function ($date) {
                $carbon = Carbon::parse($date);
                return sprintf('week_%s_%s', $carbon->format('W'), $carbon->year);
            })
            ->unique();
    }

    public function createOrUpdateMosaic(Carbon $date, $offset)
    {
        $lpadWeek =  str_pad($date->isoWeek(), 2, '0', STR_PAD_LEFT);
        $name = sprintf('Week_%s_%s', $lpadWeek, $date->isoWeekYear());
        return $this->mosaics()->updateOrCreate(
            [
                'name' => $name,
            ],
            [
                'name'     => $name,
                'path'     => sprintf('%s/%s/%s',
                    $this->download_path,
                    'mosaics',
                    sprintf('week_%s_%s.tif', $lpadWeek, $date->isoWeekYear())
                ),
                'end_date' => $date->format('Y-m-d'),
                'offset'   => $offset,
            ]);
    }
}
