<?php

namespace App\Services;

use App\NewCarRepository;
use App\Traits\EnsuresVehicleAttribute;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Mtc\ContentManager\Facades\Media;
use Mtc\MercuryDataModels\BodyStyleType;
use Mtc\MercuryDataModels\Dealership;
use Mtc\MercuryDataModels\DrivetrainType;
use Mtc\MercuryDataModels\Form;
use Mtc\MercuryDataModels\Franchise;
use Mtc\MercuryDataModels\NewCar;
use Mtc\MercuryDataModels\Template;
use Mtc\MercuryDataModels\TransmissionType;
use Mtc\MercuryDataModels\VehicleAttribute;
use Mtc\MercuryDataModels\VehicleAttributeValue;
use Mtc\MercuryDataModels\VehicleMake;
use Mtc\MercuryDataModels\VehicleModel;

class NewCarBulkUpdateService
{
    use EnsuresVehicleAttribute;

    /**
     * Final-step actions. When new-car versioning is enabled, `publish` marks
     * the snapshot version active and `draft` ("Save as version") leaves it
     * inactive; both apply the field change live, mirroring the single-edit
     * save. No effect when versioning is disabled.
     */
    public const VERSION_ACTION_PUBLISH = 'publish';
    public const VERSION_ACTION_DRAFT = 'draft';

    private const EXCLUDED_FIELDS = [
        'seo',
        'data',
        'order',
        'type_id',
    ];

    private const BOOLEAN_FIELDS = [
        'published',
        'featured',
    ];

    private const INTEGER_FIELDS = [
        'engine_size_cc',
    ];

    private const DECIMAL_FIELDS = [
        'price',
        'monthly_price',
        'deposit',
    ];

    private const DATE_FIELDS = [
        'published_at',
    ];

    /**
     * Image fields are not model columns. They map to a MediaUse `flags` entry
     * on the new car, and a value is an array of media IDs (or media objects
     * carrying an `id`). See setImageFlag().
     */
    private const IMAGE_FIELDS = [
        'hero_image' => 'hero',
        'mobile_hero_image' => 'mobile_hero',
        'listing_image' => 'listing',
    ];

    private const RELATIONSHIP_FIELDS = [
        'make_id' => [
            'label' => 'Make',
            'model' => VehicleMake::class,
            'label_column' => 'name',
        ],
        'model_id' => [
            'label' => 'Model',
            'model' => VehicleModel::class,
            'label_column' => 'name',
        ],
        'dealership_id' => [
            'label' => 'Dealership',
            'model' => Dealership::class,
            'label_column' => 'name',
        ],
        'franchise_id' => [
            'label' => 'Franchise',
            'model' => Franchise::class,
            'label_column' => 'name',
        ],
        'drivetrain_id' => [
            'label' => 'Drivetrain',
            'model' => DrivetrainType::class,
            'label_column' => 'name',
        ],
        'body_style_id' => [
            'label' => 'Body Style',
            'model' => BodyStyleType::class,
            'label_column' => 'name',
        ],
        'transmission_id' => [
            'label' => 'Transmission',
            'model' => TransmissionType::class,
            'label_column' => 'name',
        ],
        'form_id' => [
            'label' => 'Enquiry Form',
            'model' => Form::class,
            'label_column' => 'name',
        ],
        'test_drive_form_id' => [
            'label' => 'Test Drive Form',
            'model' => Form::class,
            'label_column' => 'name',
        ],
        'template_id' => [
            'label' => 'Template',
            'model' => Template::class,
            'label_column' => 'name',
        ],
    ];

    private const SELECT_FIELDS = [
        'vehicle_type' => [
            'label' => 'Vehicle Type',
            'options' => [
                ['value' => 'car', 'label' => 'Car'],
                ['value' => 'lcv', 'label' => 'LCV'],
                ['value' => 'motorcycle', 'label' => 'Motorcycle'],
            ],
        ],
    ];

    private const FIELD_LABELS = [
        'published' => 'Published',
        'featured' => 'Featured',
        'name' => 'Name',
        'derivative' => 'Derivative',
        'vehicle_type' => 'Vehicle Type',
        'price' => 'Price',
        'monthly_price' => 'Monthly Price',
        'deposit' => 'Deposit',
        'published_at' => 'Published At',
        'engine_size_cc' => 'Engine Size (cc)',
        'brochure_url' => 'Brochure URL',
        'colour' => 'Colour',
        'excerpt' => 'Excerpt',
        'hero_image' => 'Hero Image',
        'mobile_hero_image' => 'Mobile Hero Image',
        'listing_image' => 'Listing Image',
    ];

    public function getFields(): array
    {
        $newCar = new NewCar();
        $fillable = $newCar->getFillable();
        $fields = [];

        foreach ($fillable as $field) {
            if (in_array($field, self::EXCLUDED_FIELDS)) {
                continue;
            }

            $fieldData = $this->getFieldMetadata($field);
            if ($fieldData) {
                $fields[] = $fieldData;
            }
        }

        // Add image fields (hero / mobile hero / listing). These are stored as
        // MediaUse flags rather than model columns, so they are added explicitly.
        foreach (array_keys(self::IMAGE_FIELDS) as $field) {
            $fields[] = [
                'name' => $field,
                'label' => self::FIELD_LABELS[$field] ?? $this->generateLabel($field),
                'type' => 'image',
                'nullable' => true,
            ];
        }

        // Add dynamic vehicle attribute fields
        $attributes = VehicleAttribute::query()->where('model', 'new-car')->get();
        foreach ($attributes as $attribute) {
            $fields[] = $this->getAttributeFieldMetadata($attribute);
        }

        usort($fields, fn ($a, $b) => strcmp($a['label'], $b['label']));

        return $fields;
    }

    public function getAllowedFields(): array
    {
        return array_column($this->getFields(), 'name');
    }

    public function getNewCarData(array $ids): array
    {
        $allowedFields = $this->getAllowedFields();
        $modelFields = array_filter(
            $allowedFields,
            fn ($f) => !$this->isAttributeField($f) && !$this->isImageField($f)
        );

        return NewCar::query()
            ->whereIn('id', $ids)
            ->with(['attributeValues', 'mediaUses.media'])
            ->get()
            ->mapWithKeys(function (NewCar $newCar) use ($modelFields, $allowedFields) {
                $data = collect($newCar->toArray())
                    ->only($modelFields)
                    ->toArray();

                // Add attribute values
                foreach ($allowedFields as $field) {
                    if ($this->isAttributeField($field)) {
                        $attributeId = $this->getAttributeId($field);
                        $attributeValue = $newCar->attributeValues
                            ->where('attribute_id', $attributeId)
                            ->first();
                        $data[$field] = $attributeValue?->getValue() ?? null;
                    }
                }

                // Add current images per flag, shaped for the media picker.
                foreach (self::IMAGE_FIELDS as $field => $flag) {
                    $data[$field] = $newCar->mediaUses
                        ->filter(fn ($use) => !empty($use->flags[$flag]) && $use->media !== null)
                        ->map(fn ($use) => $this->mediaDescriptor($use->media))
                        ->values()
                        ->toArray();
                }

                return [$newCar->id => $data];
            })
            ->toArray();
    }

    public function update(
        string $mode,
        array $ids,
        array $data,
        string $versionAction = self::VERSION_ACTION_PUBLISH
    ): int {
        $allowedFields = $this->getAllowedFields();

        return DB::transaction(function () use ($mode, $ids, $data, $allowedFields, $versionAction) {
            if ($mode === 'same') {
                return $this->updateSameValue($ids, $data['field'], $data['value'], $allowedFields, $versionAction);
            }

            return $this->updateDifferentValues($ids, $data['updates'], $allowedFields, $versionAction);
        });
    }

    /**
     * Whether new-car versioning is enabled for the current tenant. Drives the
     * final-step "Save & publish" / "Save as version" choice in the UI.
     */
    public function versioningEnabled(): bool
    {
        return app(NewCarRepository::class)->hasVersioning();
    }

    /**
     * Persist a per-model field change and, when versioning is enabled, retain
     * a version snapshot. saveBulkVersion runs while the model is still dirty
     * so the snapshot's `data` captures the new values.
     */
    private function persistWithVersion(NewCar $car, string $versionAction): void
    {
        if ($this->versioningEnabled()) {
            app(NewCarRepository::class)->saveBulkVersion(
                $car,
                (int) Auth::id(),
                $versionAction === self::VERSION_ACTION_PUBLISH
            );
        }

        $car->save();
    }

    public function deleteFinanceExamples(array $ids): int
    {
        $deletedCount = 0;

        $newCars = NewCar::query()->whereIn('id', $ids)->get();

        foreach ($newCars as $newCar) {
            $deleted = $newCar->finance()->delete();
            if ($deleted > 0) {
                $deletedCount++;
            }
        }

        return $deletedCount;
    }

    /**
     * Soft-delete the selected new cars via per-model delete() so SoftDeletes
     * events and audit rows fire. Already-archived rows are skipped.
     */
    public function archive(array $ids): int
    {
        $archived = 0;
        $newCars = NewCar::query()->whereIn('id', $ids)->get();

        foreach ($newCars as $newCar) {
            if ($newCar->trashed()) {
                continue;
            }
            $newCar->delete();
            $archived++;
        }

        return $archived;
    }

    private function updateSameValue(
        array $ids,
        string $field,
        mixed $value,
        array $allowedFields,
        string $versionAction = self::VERSION_ACTION_PUBLISH
    ): int {
        if (!in_array($field, $allowedFields)) {
            throw new \InvalidArgumentException("Field '{$field}' is not allowed for bulk update.");
        }

        if ($this->isImageField($field)) {
            $flag = self::IMAGE_FIELDS[$field];
            $mediaIds = $this->extractMediaIds($value);

            $newCars = NewCar::query()->whereIn('id', $ids)->get();
            foreach ($newCars as $newCar) {
                $this->setImageFlag($newCar, $flag, $mediaIds);
            }

            return $newCars->count();
        }

        if ($this->isAttributeField($field)) {
            $attributeId = $this->getAttributeId($field);
            $attribute = VehicleAttribute::query()->find($attributeId);
            if (!$attribute) {
                return 0;
            }

            $newCars = NewCar::query()->whereIn('id', $ids)->get();
            foreach ($newCars as $newCar) {
                $this->updateAttributeValue($newCar, $attribute, $value);
            }

            return $newCars->count();
        }

        $value = $this->castValue($field, $value);

        // Fast path when versioning is off: single mass update, no per-model
        // events (unchanged behaviour for non-versioning tenants).
        if (!$this->versioningEnabled()) {
            return NewCar::query()
                ->whereIn('id', $ids)
                ->update([$field => $value]);
        }

        $newCars = NewCar::query()->whereIn('id', $ids)->get();
        foreach ($newCars as $newCar) {
            $newCar->{$field} = $value;
            $this->persistWithVersion($newCar, $versionAction);
        }

        return $newCars->count();
    }

    private function updateDifferentValues(
        array $ids,
        array $updates,
        array $allowedFields,
        string $versionAction = self::VERSION_ACTION_PUBLISH
    ): int {
        $updatedCount = 0;
        $versioning = $this->versioningEnabled();

        foreach ($updates as $newCarId => $fields) {
            if (!in_array((int) $newCarId, $ids)) {
                continue;
            }

            $modelFields = [];
            $attributeFields = [];
            $imageFields = [];

            foreach ($fields as $field => $value) {
                if (!in_array($field, $allowedFields)) {
                    continue;
                }

                if ($this->isImageField($field)) {
                    $imageFields[$field] = $value;
                } elseif ($this->isAttributeField($field)) {
                    $attributeFields[$field] = $value;
                } else {
                    $modelFields[$field] = $this->castValue($field, $value);
                }
            }

            $updated = false;
            // Load the model once when versioning is on (needed for the
            // snapshot), or when there are attribute/image values to write.
            $newCar = ($versioning || !empty($attributeFields) || !empty($imageFields))
                ? NewCar::query()->find($newCarId)
                : null;

            if (!empty($modelFields)) {
                if ($versioning && $newCar) {
                    $newCar->fill($modelFields);
                    $this->persistWithVersion($newCar, $versionAction);
                    $updated = true;
                } else {
                    $result = NewCar::query()
                        ->where('id', $newCarId)
                        ->update($modelFields);
                    if ($result) {
                        $updated = true;
                    }
                }
            }

            if ($newCar && !empty($attributeFields)) {
                foreach ($attributeFields as $field => $value) {
                    $attributeId = $this->getAttributeId($field);
                    $attribute = VehicleAttribute::query()->find($attributeId);
                    if ($attribute) {
                        $this->updateAttributeValue($newCar, $attribute, $value);
                        $updated = true;
                    }
                }
            }

            if ($newCar && !empty($imageFields)) {
                foreach ($imageFields as $field => $value) {
                    $this->setImageFlag($newCar, self::IMAGE_FIELDS[$field], $this->extractMediaIds($value));
                    $updated = true;
                }
            }

            if ($updated) {
                $updatedCount++;
            }
        }

        return $updatedCount;
    }

    private function getFieldMetadata(string $field): ?array
    {
        $base = [
            'name' => $field,
            'label' => self::FIELD_LABELS[$field] ?? $this->generateLabel($field),
            'nullable' => true,
        ];

        if (array_key_exists($field, self::RELATIONSHIP_FIELDS)) {
            $config = self::RELATIONSHIP_FIELDS[$field];
            return array_merge($base, [
                'type' => 'select',
                'label' => $config['label'],
                'options' => $this->getRelationshipOptions($config['model'], $config['label_column']),
            ]);
        }

        if (array_key_exists($field, self::SELECT_FIELDS)) {
            $config = self::SELECT_FIELDS[$field];
            return array_merge($base, [
                'type' => 'select',
                'label' => $config['label'],
                'options' => $config['options'],
            ]);
        }

        if (in_array($field, self::BOOLEAN_FIELDS)) {
            return array_merge($base, [
                'type' => 'boolean',
                'nullable' => false,
            ]);
        }

        if (in_array($field, self::INTEGER_FIELDS)) {
            return array_merge($base, [
                'type' => 'integer',
            ]);
        }

        if (in_array($field, self::DECIMAL_FIELDS)) {
            return array_merge($base, [
                'type' => 'decimal',
            ]);
        }

        if (in_array($field, self::DATE_FIELDS)) {
            return array_merge($base, [
                'type' => 'date',
            ]);
        }

        return array_merge($base, [
            'type' => 'string',
        ]);
    }

    private function getAttributeFieldMetadata(VehicleAttribute $attribute): array
    {
        $type = match ($attribute->type) {
            'text', 'textarea', 'editor' => 'string',
            'number' => 'decimal',
            'integer' => 'integer',
            'boolean' => 'boolean',
            'datetime' => 'date',
            'select' => 'select',
            default => 'string',
        };

        $field = [
            'name' => "attribute::{$attribute->id}",
            'label' => $attribute->name,
            'type' => $type,
            'nullable' => true,
        ];

        if ($type === 'select' && !empty($attribute->data)) {
            $field['options'] = collect($attribute->data)->map(fn ($option) => [
                'value' => $option,
                'label' => $option,
            ])->toArray();
        }

        if ($type === 'boolean') {
            $field['nullable'] = false;
        }

        return $field;
    }

    private function getRelationshipOptions(string $modelClass, string $labelColumn): array
    {
        return $modelClass::query()
            ->select(['id', $labelColumn])
            ->orderBy($labelColumn)
            ->get()
            ->map(fn ($item) => [
                'value' => $item->id,
                'label' => $item->{$labelColumn},
            ])
            ->toArray();
    }

    private function generateLabel(string $field): string
    {
        return ucwords(str_replace('_', ' ', $field));
    }

    private function castValue(string $field, mixed $value): mixed
    {
        if ($value === null || $value === '') {
            return null;
        }

        if (in_array($field, self::BOOLEAN_FIELDS)) {
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
        }

        if (in_array($field, self::INTEGER_FIELDS)) {
            return (int) $value;
        }

        if (in_array($field, self::DECIMAL_FIELDS)) {
            return (float) $value;
        }

        return $value;
    }

    private function isAttributeField(string $field): bool
    {
        return str_starts_with($field, 'attribute::');
    }

    private function isImageField(string $field): bool
    {
        return array_key_exists($field, self::IMAGE_FIELDS);
    }

    /**
     * Normalise an image value into a list of media IDs. The UI submits an
     * array of media objects (each carrying an `id`), but a plain array of IDs
     * is also accepted.
     */
    private function extractMediaIds(mixed $value): array
    {
        if (!is_array($value)) {
            return [];
        }

        return collect($value)
            ->map(fn ($item) => is_array($item) ? ($item['id'] ?? null) : $item)
            ->filter(fn ($id) => is_numeric($id))
            ->map(fn ($id) => (int) $id)
            ->unique()
            ->values()
            ->all();
    }

    private function mediaDescriptor($media): array
    {
        return [
            'id' => $media->id,
            'title' => $media->title,
            'src' => $media->square_thumb_url,
            'original_src' => $media->original_url,
            'type' => $media->type,
            'extension' => $media->extension,
        ];
    }

    /**
     * Set which media carry a given image flag (hero / mobile_hero / listing)
     * for a new car, without disturbing the other image flags.
     *
     * Existing media keep their other flags; a media that ends up with no flags
     * is removed. Target media have the flag merged onto any flags they already
     * hold (a single image can be both hero and listing).
     *
     * @param int[] $mediaIds
     */
    private function setImageFlag(NewCar $newCar, string $flag, array $mediaIds): void
    {
        $newCar->load('mediaUses');

        // Drop the flag from media that should no longer carry it.
        foreach ($newCar->mediaUses as $use) {
            $flags = $use->flags ?? [];
            if (empty($flags[$flag]) || in_array((int) $use->media_id, $mediaIds, true)) {
                continue;
            }

            unset($flags[$flag]);
            if (empty($flags)) {
                $use->delete();
            } else {
                $use->flags = $flags;
                $use->save();
            }
        }

        // Apply the flag to each target media, preserving its existing flags.
        foreach ($mediaIds as $mediaId) {
            $existing = $newCar->mediaUses->firstWhere('media_id', $mediaId);
            $flags = $existing?->flags ?? [];
            $flags[$flag] = true;
            Media::setUsesForModel([$mediaId], $newCar, ['flags' => $flags], false);
        }
    }

    private function getAttributeId(string $field): int
    {
        return (int) str_replace('attribute::', '', $field);
    }

    private function updateAttributeValue(NewCar $newCar, VehicleAttribute $attribute, mixed $value): void
    {
        $column = $this->attributeValueColumn($attribute->toArray());

        if ($value === null || $value === '') {
            $newCar->attributeValues()
                ->where('attribute_id', $attribute->id)
                ->delete();
            return;
        }

        VehicleAttributeValue::query()->updateOrCreate(
            [
                'attribute_id' => $attribute->id,
                'owner_id' => $newCar->id,
                'owner_type' => $newCar->getMorphClass(),
                'index' => 0,
            ],
            [
                'type' => $attribute->type,
                'slug' => $attribute->slug,
                $column => $value,
            ]
        );
    }
}
