<?php

namespace App\Jobs;

use App\Mail\OrphanedTaxonomyRemovedMail;
use App\TaxonomyMap;
use App\TaxonomyMapable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Mail;
use Mtc\MercuryDataModels\NewCar;
use Mtc\MercuryDataModels\Tenant;
use Mtc\MercuryDataModels\Vehicle;
use Mtc\MercuryDataModels\VehicleOffer;

class RemoveOrphanedTaxonomiesFromModelsJob implements ShouldQueue, ShouldBeUnique
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    private Collection $orphaned_taxonomy_makes;
    private Collection $orphaned_taxonomy_models;
    private Collection $affected_models;

    /**
     * Create a new job instance.
     */
    public function __construct(private Collection $orphaned_taxonomy_maps)
    {
        $this->orphaned_taxonomy_makes = $this->orphaned_taxonomy_maps
            ->filter(fn ($map) => in_array($map->taxonomy_type, ['make', 'master-make']));

        $this->orphaned_taxonomy_models = $this->orphaned_taxonomy_maps
            ->filter(fn ($map) => in_array($map->taxonomy_type, ['model', 'master-model']));
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        if (
            $this->orphaned_taxonomy_makes->isNotEmpty()
            || $this->orphaned_taxonomy_models->isNotEmpty()
        ) {
            $affected_tenants = Tenant::query()->get()->mapWithKeys(fn ($tenant) => [
                $tenant->name => $this->processOrphanedMapsForTenant($tenant),
            ])->filter(fn ($records) => $records['vehicles']->isNotEmpty()
                || $records['vehicle_offers']->isNotEmpty()
                || $records['new_cars']->isNotEmpty());

            $this->sendNotification($affected_tenants);
        }

        $this->deleteOrphanedMaps();
    }

    private function processOrphanedMapsForTenant(Tenant $tenant): Collection
    {
        tenancy()->initialize($tenant);

        $affected_vehicles = $this->handleforModel(Vehicle::class);
        $affected_vehicle_offers = $this->handleforModel(VehicleOffer::class);
        $affected_new_cars = $this->handleforModel(NewCar::class);

        return $this->mapAffectedRecordData($affected_vehicles, $affected_vehicle_offers, $affected_new_cars);
    }

    private function mapAffectedRecordData(
        Collection $affected_vehicles,
        Collection $affected_vehicle_offers,
        Collection $affected_new_cars,
    ): Collection {
        return collect([
            'vehicles' => $affected_vehicles->map(fn ($vehicle) => [
                'id' => $vehicle->id,
                'name' => $vehicle->title,
                'slug' => $vehicle->slug,
                'vrm' => $vehicle->vrm_condensed,
            ]),
            'vehicle_offers' => $affected_vehicle_offers->map(fn ($offer) => [
                'id' => $offer->id,
                'name' => $offer->name,
                'slug' => $offer->slug,
            ]),
            'new_cars' => $affected_new_cars->map(fn ($new_car) => [
                'id' => $new_car->id,
                'name' => $new_car->name,
                'slug' => $new_car->slug,
            ]),
        ]);
    }

    /**
     * return the list of models affected by missing make and/or model taxonomy
     *
     * @param string $model_name
     * @return Collection
     */
    private function handleforModel(string $model_name): Collection
    {
        $affected_by_make = $this->setAffectedModelColumnToNull(
            $model_name,
            'make_id',
            $this->orphaned_taxonomy_makes
        );

        $affected_by_model = $this->setAffectedModelColumnToNull(
            $model_name,
            'model_id',
            $this->orphaned_taxonomy_models
        );

        return $affected_by_make->merge($affected_by_model);
    }

    private function setAffectedModelColumnToNull(
        string $model_name,
        string $column,
        Collection $orphaned_taxonomy_items
    ): Collection {
        return App::make($model_name)->query()
            ->whereIn($column, $orphaned_taxonomy_items->pluck('taxonomy_id'))
            ->get()
            ->mapWithKeys(function ($affected) use ($column) {
                $affected->update([
                    $column => null,
                ]);

                return [
                    $affected->id => $affected,
                ];
            });
    }

    /**
     * Delete orphaned maps, but never one that is still referenced by a mapable
     * pointing at a record that still exists on a tenant. Dead mapables (whose
     * target no longer exists) are pruned regardless of whether their map survives.
     */
    private function deleteOrphanedMaps(): void
    {
        $orphaned_map_ids = $this->orphaned_taxonomy_maps->pluck('id');

        $mapables = TaxonomyMapable::query()
            ->whereIn('taxonomy_map_id', $orphaned_map_ids)
            ->get();

        $live_map_ids = collect();
        $dead_mapable_ids = collect();

        $mapables->groupBy('tenant')->each(function (Collection $tenant_mapables, $tenant_id) use (
            &$live_map_ids,
            &$dead_mapable_ids
        ) {
            $tenant = Tenant::find($tenant_id);

            if ($tenant === null) {
                $dead_mapable_ids = $dead_mapable_ids->merge($tenant_mapables->pluck('id'));

                return;
            }

            tenancy()->initialize($tenant);

            foreach ($tenant_mapables as $mapable) {
                if ($this->mapableTargetExists($mapable)) {
                    $live_map_ids->push($mapable->taxonomy_map_id);
                } else {
                    $dead_mapable_ids->push($mapable->id);
                }
            }
        });

        if ($dead_mapable_ids->isNotEmpty()) {
            TaxonomyMapable::query()->whereIn('id', $dead_mapable_ids->unique())->delete();
        }

        $deletable_map_ids = $orphaned_map_ids->diff($live_map_ids->unique());

        if ($deletable_map_ids->isNotEmpty()) {
            TaxonomyMap::query()->whereIn('id', $deletable_map_ids)->delete();
        }
    }

    private function mapableTargetExists(TaxonomyMapable $mapable): bool
    {
        $model_class = Relation::getMorphedModel($mapable->mappable_type) ?? $mapable->mappable_type;

        if (!is_string($model_class) || !class_exists($model_class)) {
            return false;
        }

        return $model_class::query()->whereKey($mapable->mappable_id)->exists();
    }

    private function sendNotification(Collection $affected_tenants): void
    {
        Mail::to(config('mail.developer_alerts'))->send(new OrphanedTaxonomyRemovedMail(
            $this->orphaned_taxonomy_makes,
            $this->orphaned_taxonomy_models,
            $affected_tenants,
        ));
    }
}
