<?php

namespace App\Master;

use App\CatalogOfferRepository;
use App\ContentCategoryRepository;
use App\ContentTagRepository;
use App\Contracts\InteractsWithContentSync;
use App\DealershipRepository;
use App\ElementRepository;
use App\EnquiryStatusRepository;
use App\EnquiryTypeRepository;
use App\FormRepository;
use App\FranchiseRepository;
use App\GlobalContentRepository;
use App\MenuRepository;
use App\MediaSizeRepository;
use App\Models\ContentSync;
use App\NewCarRepository;
use App\OfferRepository;
use App\PageRepository;
use App\PropertyRepository;
use App\TemplateRepository;
use App\Traits\FormatAsCurrency;
use App\ValuationAdjustmentRepository;
use App\VehicleAttributeRepository;
use App\VehicleTrimRepository;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Mtc\ContentManager\Models\ContentCategory;
use Mtc\ContentManager\Models\ContentTag;
use Mtc\ContentManager\Models\MediaSize;
use Mtc\Crm\Models\EnquiryStatus;
use Mtc\Crm\Models\EnquiryType;
use Mtc\MercuryDataModels\CatalogOffer;
use Mtc\MercuryDataModels\ContentElement;
use Mtc\MercuryDataModels\Dealership;
use Mtc\MercuryDataModels\Form;
use Mtc\MercuryDataModels\Franchise;
use Mtc\MercuryDataModels\GlobalContent;
use Mtc\MercuryDataModels\Menu;
use Mtc\MercuryDataModels\NewCar;
use Mtc\MercuryDataModels\Page;
use Mtc\MercuryDataModels\Property;
use Mtc\MercuryDataModels\Template;
use Mtc\MercuryDataModels\ValuationAdjustment;
use Mtc\MercuryDataModels\VehicleAttribute;
use Mtc\MercuryDataModels\VehicleOffer;
use Mtc\MercuryDataModels\VehicleTrim;

class ContentSyncRepository
{
    use FormatAsCurrency;

    /**
     * @param array $input
     * @return ContentSync|Model
     */
    public function provideToken(array $input): ContentSync
    {
        return ContentSync::query()
            ->create([
                'details' => collect($input)->except([
                    'tenant_id',
                    'token',
                ]),
                'user_id' => Auth::id(),
                'token' => Str::random(50),
                'tenant_id' => $input['tenant_id'],
            ]);
    }

    /**
     * @param array $input
     * @return array
     * @throws \Exception
     */
    public function attemptSync(array $input): array
    {
        return match ($input['direction']) {
            ContentSync::PUSH => $this->sendDataToRemote($input),
            ContentSync::PULL => $this->preparePullImport($input),
        };
    }

    public function provideData(array $input): array
    {
        if (!empty($input['selections'])) {
            return $this->getDataRepository($input['scope'])->exportToRemote($input['selections']);
        }

        return match ($input['scope']) {
            'pages' => Page::query()
                ->select(['id', 'title as name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
            'templates' => Template::query()
                ->select(['id', 'name'])
                ->orderBy('name')
                ->active()
                ->get()
                ->toArray(),
            'menus' => Menu::query()
                ->select(['id', 'title as name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
            'global-content' => GlobalContent::query()
                ->select(['id', 'name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
            'elements' => ContentElement::query()
                ->select(['id', 'title as name'])
                ->when(!empty($input['filter']), fn ($query) => $query->where('category', $input['filter']))
                ->orderBy('name')
                ->active()
                ->get()
                ->toArray(),
            'media-library-sizes' => MediaSize::query()
                ->orderBy('model')
                ->orderBy('label')
                ->get()
                ->map(fn ($size) => ['id' => $size->id, 'name' => $size->model . ': ' . $size->label])
                ->toArray(),
            'trims' => VehicleTrim::query()
                ->select(['id', 'name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
            'valuation-adjustments' => ValuationAdjustment::query()
                ->orderBy('is_increase')
                ->orderBy('adjustment_type')
                ->orderBy('adjustment_amount')
                ->get()
                ->map(function ($adjustment) {
                    $amount = $adjustment->adjustment_type === 'percent'
                        ? $adjustment->adjustment_amount . '%'
                        : $this->asCurrency($adjustment->adjustment_amount, tenant()->currency);
                    return [
                        'id' => $adjustment->id,
                        'name' => $adjustment->is_increase
                            ? __('valuations.adjustment_increase', ['amount' => $amount])
                            : __('valuations.adjustment_decrease', ['amount' => $amount])
                    ];
                })
                ->toArray(),
            'vehicle-attributes' => VehicleAttribute::query()
                ->select(['id', 'name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
            'catalog-offers' => CatalogOffer::query()
                ->select(['id', 'name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
            'content-tags' => ContentTag::query()
                ->select(['id', 'name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
            'content-categories' => ContentCategory::query()
                ->select(['id', 'name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
            'enquiry-statuses' => EnquiryStatus::query()
                ->select(['id', 'name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
            'enquiry-types' => EnquiryType::query()
                ->select(['id', 'name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
            'data-modules' => Property::query()
                ->with('category')
                ->get()
                ->map(fn ($property) => [
                    'id' => $property->id,
                    'name' => $property->category->name . ': ' . $property->name,
                ])
                ->sortBy('name')
                ->toArray(),
            'franchises' => Franchise::query()
                ->select(['id', 'name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
            'dealerships' => Dealership::query()
                ->select(['id', 'name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
            'new-vehicles' => NewCar::query()
                ->select(['id', 'name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
            'offers' => VehicleOffer::query()
                ->select(['id', 'name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
            'forms' => Form::query()
                ->select(['id', 'name'])
                ->orderBy('name')
                ->get()
                ->toArray(),
        };
    }

    public function validateImportData(?array $data, string $scope): Collection
    {
        return collect($data ?? [])
            ->map(fn ($entry) => $this->getDataRepository($scope)->checkImportEntryValidity($entry, $data ?? []));
    }

    /**
     * @throws \Exception
     */
    public function pullDataFromRemote(array $input): array
    {
        $response = Http::timeout(5)
            ->withHeaders([
                'x-token' => $input['token'],
                'x-user' => Auth::user()->email,
            ])
            ->post($this->endpoint($input, 'provide-entries'), $this->pullDataDetails($input));


        if ($response->failed()) {
            Log::warning($response->body());
            throw new \Exception('Failed to sync with status ' . $response->status());
        }

        return $response->json();
    }


    /**
     * @throws \Exception
     */
    private function sendDataToRemote(array $input): array
    {
        $response = Http::timeout(5)
            ->withHeaders([
                'x-token' => $input['token'],
                'x-user' => Auth::user()->email,
            ])
            ->put($this->endpoint($input), $this->pushDataDetails($input));

        if ($response->failed()) {
            throw new \Exception('Failed to sync with status ' . $response->status());
        }

        return [];
    }

    public function importData(array $details, array $data): Response
    {
        try {
            // This was nested with data + errors when sent to UI
            $importData = collect($data)->map(fn ($datum) => $datum['data'] ?? null)->filter()->toArray();
            tenancy()->initialize($details['tenant_id']);
            DB::beginTransaction();
            $result = $this->importAllEntries($importData, $this->getDataRepository($details['scope']));
            DB::commit();
            tenancy()->end();
            if ($result) {
                $this->markCompleted();
            }
            return \response([
                'status' => 'ok',
            ]);
        } catch (ModelNotFoundException $exception) {
            $this->markFailed($exception, $details, $data);

            return response([
                'status' => 'error',
                'message' => ' Missing ' . $exception->getModel() . ' element',
            ], 400);
        } catch (\Exception $exception) {
            $this->markFailed($exception, $details, $data);

            return response([
                'status' => 'error',
                'message' => ' Import failed due to error: ' . $exception->getMessage(),
            ], 400);
        }
    }

    public function upsertDetails(Request $request)
    {
        $sync = ContentSync::query()
            ->where('token', $request->input('token'))
            ->where('created_at', '>', Carbon::now()->subMinutes(ContentSync::TTL))
            ->firstOrFail();

        return [
            'tenant_id' => $sync->tenant_id,
            'scope' => $request->input('scope'),
            'data' => $request->input('with_data'),
        ];
    }

    private function endpoint(array $input, $path = 'perform'): string
    {
        $host = match ($input['environment']) {
            'prod' => config('services.sync.prod_url'),
            'vps' => config('services.sync.vps_url'),
            'custom' => $input['customEnvUrl'],
        };

        return rtrim((string) $host, '/') . '/api/content-sync/' . $path;
    }

    private function importAllEntries(array $data, InteractsWithContentSync $repository): bool
    {
        $outstanding = collect($data);
        $i = 0;
        do {
            $imported = $outstanding
                ->filter(fn ($entry) => Log::debug('import entry', [
                        $repository->canBeImported($entry),
                        $entry
                    ]) || $repository->canBeImported($entry))
                ->map(fn ($entry, $index) => $repository->importRecord($entry) ? $index : -1)
                ->values();
            $outstanding = $outstanding->forget($imported->toArray());

            if ($i++ > 100) {
                throw new \Exception('Import over 100 loops, unable to import');
            }
        } while ($outstanding->isNotEmpty());
        return true;
    }

    private function pullDataDetails(array $input): array
    {
        return [
            'scope' => $input['scope'],
            'selections' => $input['selections'],
        ];
    }

    private function pushDataDetails(array $input): array
    {
        tenancy()->initialize($input['tenant_id']);
        $data = $this->getDataRepository($input['scope'])->exportToRemote($input['selections']);
        tenancy()->end();
        return [
            'scope' => $input['scope'],
            'with_data' => $input['data'],
            'data' => $data,
        ];
    }

    private function markCompleted()
    {
    }

    private function markFailed(ModelNotFoundException|\Exception $exception, array $details, array $data)
    {
    }

    private function getDataRepository(string $type): InteractsWithContentSync
    {
        return match ($type) {
            'elements' => App::make(ElementRepository::class),
            'templates' => App::make(TemplateRepository::class),
            'global-content' => App::make(GlobalContentRepository::class),
            'pages' => App::make(PageRepository::class),
            'menus' => App::make(MenuRepository::class),
            'media-library-sizes' => App::make(MediaSizeRepository::class),
            'trims' => App::make(VehicleTrimRepository::class),
            'valuation-adjustments' => App::make(ValuationAdjustmentRepository::class),
            'vehicle-attributes' => App::make(VehicleAttributeRepository::class),
            'catalog-offers' => App::make(CatalogOfferRepository::class),
            'content-tags' => App::make(ContentTagRepository::class),
            'content-categories' => App::make(ContentCategoryRepository::class),
            'enquiry-statuses' => App::make(EnquiryStatusRepository::class),
            'enquiry-types' => App::make(EnquiryTypeRepository::class),
            'data-modules' => App::make(PropertyRepository::class),
            'franchises' => App::make(FranchiseRepository::class),
            'dealerships' => App::make(DealershipRepository::class),
            'new-vehicles' => App::make(NewCarRepository::class),
            'offers' => App::make(OfferRepository::class),
            'forms' => App::make(FormRepository::class),
        };
    }

    private function preparePullImport(array $input): array
    {
        $sync = ContentSync::query()
            ->create([
                'details' => collect($input)->except([
                    'tenant_id',
                    'token',
                ]),
                'user_id' => Auth::id(),
                'token' => $input['token'],
                'tenant_id' => $input['tenant_id'],
            ]);

        return [
            'status' => 'ok',
            'id' => $sync->id,
        ];
    }

    public function renewToken(ContentSync $sync): ContentSync
    {
        $sync->created_at = now();
        $sync->save();

        return $sync;
    }
}
