<?php

namespace App\Services;

use App\Crm\Config\EnquiryMaxConfig;
use App\Facades\Settings;
use App\Facades\Site;
use Carbon\Carbon;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Mtc\Crm\Contracts\EnquiryActionModel;
use Mtc\Crm\Contracts\EnquiryModel;
use Mtc\Crm\Models\FormQuestion;
use Mtc\MercuryDataModels\ApiNotification;
use Mtc\MercuryDataModels\Dealership;
use Mtc\MercuryDataModels\Vehicle;
use Mtc\MercuryDataModels\VehicleOffer;
use Mtc\MercuryDataModels\VehicleValuation;
use Mtc\VehicleReservations\Reservation;

class EnquiryMaxApi
{
    /**
     * List of dealers returned from EnquiryMax
     *
     * @var Collection
     */
    private Collection $dealers;

    /**
     * Auth token
     *
     * @var string
     */
    private string $token;

    /**
     * API response
     *
     * @var Response
     */
    private Response $response;

    /**
     * EnquiryMaxApi constructor
     *
     * @param EnquiryMaxConfig $config
     * @throws AuthenticationException
     */
    public function __construct(private readonly EnquiryMaxConfig $config)
    {
        $this->authenticate();
    }

    /**
     * Send a lead to EnquiryMax
     *
     * @param EnquiryModel $enquiry
     * @param EnquiryActionModel $action
     * @return bool
     */
    public function sendLead(EnquiryModel $enquiry, EnquiryActionModel $action): bool
    {
        $data = $this->mapEnquiryData($enquiry);

        $dealer_id = Settings::get('crm-enquiry-max-closest-dealer')
            ? $this->dealerIdByDistance($enquiry, $action, $data)
            : $this->dealerId($enquiry, $action);

        return $this->submitLead($data, $dealer_id, $enquiry->id);
    }

    public function sendReservation(Reservation $reservation): bool
    {
        $data = $this->mapReservationData($reservation);
        $dealer = $reservation->vehicle?->dealership?->data['enquiry-max-dealership'] ?? '';

        $match = $this->dealers
            ->filter(fn($entry) => $entry['dealerName'] === $dealer)
            ->first();

        $dealer_id = $match ? $match['dealerId'] : $this->dealers->first()['dealerId'];

        return $this->submitLead($data, $dealer_id, $reservation->id);
    }

    protected function submitLead(array $data, string $dealer_id, int $reference): bool
    {
        $this->response = Http::timeout(60)->withHeaders([
            "Content-Type" => "application/json",
            "cache-control" => "no-cache",
            "Authorization" => "Bearer " . $this->token,
        ])->post($this->endpoint("services/v1/$dealer_id/lead"), $data);

        ApiNotification::query()
            ->create([
                'provider' => 'enquiry-max',
                'reference' => $reference,
                'processed' => $this->response->successful(),
                'data' => [
                    'request' => $data,
                    'result' => $this->response->json(),
                ],
            ]);


        return $this->response->status() === 200;
    }

    /**
     * Get an attribute from response
     *
     * @param string $attribute
     * @return mixed
     */
    public function getResponseAttribute(string $attribute): mixed
    {
        return $this->response->json($attribute);
    }

    /**
     * Perform authentication with API
     *
     * @return void
     * @throws AuthenticationException
     */
    private function authenticate(): void
    {
        if ($this->hasActiveToken()) {
            return;
        }

        $response = Http::post($this->endpoint('services/token'), [
            'granttype' => 'client_credentials',
            'clientId' => $this->config->clientId(),
            'clientSecret' => $this->config->clientSecret()
        ]);

        if ($response->status() === 200) {
            $this->token = $response->json('accessToken');
            $this->dealers = collect($response->json('dealers'));
            $this->persistToken($response->json());
        } else {
            Log::error('Failed to authenticate EnquiryMax', [
                'tenant' => tenant('id'),
                'client_id' => $this->config->clientId(),
                'status code' => $response->status(),
                'response data' => $response->json(),
            ]);

            throw new AuthenticationException('Failed to authenticate EnquiryMax');
        }
    }

    /**
     * Determine dealer id from enquiry
     *
     * @param EnquiryModel $enquiry
     * @param EnquiryActionModel $action
     * @return string|int
     */
    private function dealerId(EnquiryModel $enquiry, EnquiryActionModel $action): string|int
    {
        $dealer = '';
        if (in_array($enquiry->reason_type, ['vehicle', 'offer'])) {
            $dealer = $enquiry->reason->dealership->data['enquiry-max-dealership'] ?? '';
        }

        if (empty($dealer)) {
            $dealer = $action->data['fallback-dealership'] ?? '';
        }

        $match = $this->dealers
            ->filter(fn($entry) => $entry['dealerName'] === $dealer)
            ->first();

        return $match ? $match['dealerId'] : $this->dealers->first()['dealerId'];
    }

    /**
     * Http request endpoint
     *
     * @param string $path
     * @return string
     */
    private function endpoint(string $path): string
    {
        if ($this->config->mode() === 'test') {
            return 'https://stagingapi.enquirymax.net/' . ltrim($path, '/');
        }

        return 'https://api.enquirymax.net/' . ltrim($path, '/');
    }

    /**
     * Check if there is an active token for API
     * Load it exists
     *
     * @return bool
     */
    private function hasActiveToken(): bool
    {
        $data = json_decode((string) Storage::disk('file-storage')->get($this->authTokenPath()), true);
        if (empty($data)) {
            return false;
        }

        // Is expired or expires in next 10 minutes;
        $is_expired = Carbon::parse($data['expires_at']) <= Carbon::now()->subMinutes(10);

        if ($is_expired) {
            return false;
        }

        $this->token = $data['accessToken'];
        $this->dealers = collect($data['dealers']);

        return true;
    }

    /**
     * Persist token to avoid having to re-auth whilst token is active
     *
     * @param $data
     * @return bool
     */
    private function persistToken($data): bool
    {
        $data['expires_at'] = Carbon::now()->addSeconds($data['expiresIn']);
        return Storage::disk('file-storage')->put($this->authTokenPath(), json_encode($data));
    }

    /**
     * Auth token file path
     *
     * @return string
     */
    private function authTokenPath(): string
    {
        return 'enquiry-max/' . tenant('id') . '/auth-token';
    }

    /**
     * Map enquiry data to request data
     *
     * @param EnquiryModel $enquiry
     * @return array
     */
    private function mapEnquiryData(EnquiryModel $enquiry): array
    {
        $params = $this->convertEnquiryToParams($enquiry);
        return [
            "leadType" => $params['leadType'] ?? '',
            "sourceOfEnquiry" => $params['sourceOfEnquiry'] ?? 'Website',
            "methodOfContact" => $params['methodOfContact'] ?? '',
            "notes" => $params['notes'] ?? '',
            "link" => $params['link'] ?? '',

            "customer" => [
                "title" => $params['title'] ?? '',
                "forename" => $params['forename'] ?? '',
                "surname" => $params['surname'] ?? '',
                "companyName" => $params['companyName'] ?? '',
                "bestTimeToCall" => $params['bestTimeToCall'] ?? '',
                "preferedContactMethod" => $params['preferedContactMethod'] ?? '',
                "contactMethod" => $params['contactMethod'] ?? '',
                "homeTelephoneNumber" => $params['homeTelephoneNumber'] ?? '',
                "mobileTelephoneNumber" => $params['mobileTelephoneNumber'] ?? '',
                "workTelephoneNumber" => $params['workTelephoneNumber'] ?? '',
                "emailAddress" => $params['emailAddress'] ?? '',
                "address" => [
                    "line1" => $params['address.line1'] ?? '',
                    "line2" => $params['address.line2'] ?? '',
                    "line3" => $params['address.line3'] ?? '',
                    "line4" => $params['address.line4'] ?? '',
                    "postcode" => $params['address.postcode'] ?? '',
                ],
                "marketingPreferences" => [
                    "post" => empty($params['marketing.post']) ? 'false' : 'true',
                    "phone" => empty($params['marketing.phone']) ? 'false' : 'true',
                    "sms" => empty($params['marketing.sms']) ? 'false' : 'true',
                    "email" => empty($params['marketing.email']) ? 'false' : 'true',
                ],
                "vehicle" => $this->partExchangeData($enquiry),
            ],

            "vehicleOfInterest" => $this->vehicleOfInterestData($enquiry),
            "salesPerson" => [
                "username" => "",
                "displayName" => "",
                "autoAssignLead" => false
            ],
            "appointment" => [
                "appointmentDateTime" => $params['appointmentDateTime'] ?? '',
                "appointment.notes" => $params['appointmentNotes'] ?? '',
            ],
            "Misc" => $this->miscellaneousData($params),
        ];
    }

    private function miscellaneousData(array $params): array
    {
        $data = [];

        if (array_key_exists('utm_source', $params)) {
            $data['UTM Source'] = $params['utm_source'] ?? '';
        }

        if (array_key_exists('utm_medium', $params)) {
            $data['UTM Medium'] = $params['utm_medium'] ?? '';
        }

        if (array_key_exists('utm_campaign', $params)) {
            $data['UTM Campaign'] = $params['utm_campaign'] ?? '';
        }

        return $data;
    }

    /**
     * Map enquiry data to request data
     *
     * @param EnquiryModel $enquiry
     * @return array
     */
    private function mapReservationData(Reservation $reservation): array
    {
        $name = explode(' ', $reservation->name);
        $first_name = array_shift($name);
        $last_name = implode(' ', $name);
        return [
            "leadType" => 'Reservation',
            "sourceOfEnquiry" => 'Website',
            "methodOfContact" => '',
            "notes" => '',
            "link" => '',
            "customer" => [
                "title" => '',
                "forename" => $first_name,
                "surname" => $last_name,
                "companyName" => '',
                "bestTimeToCall" => '',
                "preferedContactMethod" => '',
                "contactMethod" => 'email',
                "homeTelephoneNumber" => '',
                "mobileTelephoneNumber" => $reservation->contact_number,
                "workTelephoneNumber" => '',
                "emailAddress" => $reservation->email,
                "address" => [
                    "line1" => '',
                    "line2" => '',
                    "line3" => '',
                    "line4" => '',
                    "postcode" => '',
                ],
                "marketingPreferences" => [
                    "post" => empty($params['marketing.post']) ? 'false' : 'true',
                    "phone" => empty($params['marketing.phone']) ? 'false' : 'true',
                    "sms" => empty($params['marketing.sms']) ? 'false' : 'true',
                    "email" => empty($params['marketing.email']) ? 'false' : 'true',
                ],
            ],
            "vehicleOfInterest" => [
                "isUsed" => $reservation->vehicle->is_new ? 0 : 1,
                "price" => $reservation->vehicle->price ?? 0,
                "make" => $reservation->vehicle->make->name ?? '',
                "range" => '',
                "model" => $reservation->vehicle->model->name ?? '',
                "derivative" => $reservation->vehicle->derivative ?? '',
                "vrm" => $reservation->vehicle->registration_number ?? '',
                "fuelType" => $reservation->vehicle->fuelType->name ?? '',
                "vehicleIdentificationCode" => $reservation->vehicle->cap_id ?? '',
                "odometer" => [
                    "unit" => "Miles",
                    "currentValue" => $reservation->vehicle->odometer_mi
                ],
                "stockNumber" => $reservation->vehicle->id ?? ''
            ],
            "salesPerson" => [
                "username" => "",
                "displayName" => "",
                "autoAssignLead" => false
            ],
            "appointment" => [
                "appointmentDateTime" => '',
                "appointment.notes" => '',
            ],
            "Misc" => $this->miscellaneousData([
                'utm_source' => $reservation->data['meta']['utm_source'] ?? null,
                'utm_medium' => $reservation->data['meta']['utm_medium'] ?? null,
                'utm_campaign' => $reservation->data['meta']['utm_campaign'] ?? null,
                'utm_term' => $reservation->data['meta']['utm_term'] ?? null,
                'utm_content' => $reservation->data['meta']['utm_content'] ?? null,
            ]),
        ];
    }

    /**
     * Map out questions to their respective EnquiryMax fields
     *
     * @param EnquiryModel $enquiry
     * @return array
     */
    private function convertEnquiryToParams(EnquiryModel $enquiry): array
    {
        $answers = $enquiry->getNormalizedAnswers()->keyBy('question_id')->map(fn($item) => $item['answer']);
        $params = FormQuestion::query()
            ->whereIn('id', $answers->keys())
            ->get()
            ->keyBy('id')
            ->map(fn($question) => $question->data['enquiry-max-field'] ?? null)
            ->filter()
            ->flip()
            ->map(fn($id, $key) => $answers[$id] ?? null)
            ->toArray();

        try {
            if ($enquiry->reason_type === 'vehicle') {
                if ($enquiry->reason->is_new) {
                    $stock_status = $enquiry->reason->getCustom('stock_status') ?? '';

                    if (!empty($stock_status)) {
                        $params['leadType'] = 'New Car Enquiry - ' . $stock_status;
                    } else {
                        $params['leadType'] = 'New Car Enquiry';
                    }
                } else {
                    $params['leadType'] = 'Used Car Enquiry';
                }

                $params['link'] = Site::vehicleUrl($enquiry->reason);
            } elseif ($enquiry->reason_type === 'offer') {
                $params['leadType'] = 'New Car Enquiry' . $this->offerSuffix($enquiry);
                $params['link'] = Site::offerUrl($enquiry->reason);
            } elseif (!empty($enquiry->valuation)) {
                $params['leadType'] = 'Part Exchange';
                $params['notes'] = 'Valuation: ' .
                    ($enquiry->valuation->adjusted_retail_price ?? $enquiry->valuation->retail_price);
            }
        } catch (\Throwable $throwable) {
            Log::warning($throwable->getMessage(), [
                'tenant' => tenant('id'),
                'enquiry' => $enquiry->id,
            ]);
        }

        $meta = $enquiry->data['meta'] ?? [];
        $params['utm_source'] = $params['utm_source'] ?? $meta['utm_source'] ?? null;
        $params['utm_medium'] = $params['utm_medium'] ?? $meta['utm_medium'] ?? null;
        $params['utm_campaign'] = $params['utm_campaign'] ?? $meta['utm_campaign'] ?? null;

        return $params;
    }

    /**
     * Get vehicle of interest data from enquiry source (vehicle / offer)
     *
     * @param EnquiryModel $enquiry
     * @return array
     */
    private function partExchangeData(EnquiryModel $enquiry): array
    {
        if (!empty($enquiry->valuation)) {
            /** @var VehicleValuation $valuation */
            $valuation = $enquiry->valuation;
            return [
                "dateOfRegistration" => $valuation->date_of_registration
                    ? Carbon::parse($valuation->date_of_registration)
                    : '',
                "nextMotDate" => "",
                "nextServiceDate" => "",
                "make" => $valuation->make ?? '',
                "range" => '',
                "model" => $valuation->model ?? '',
                "derivative" => $valuation->derivative ?? '',
                "vrm" => $valuation->registration ?? '',
                "fuelType" => $valuation->fuel_type ?? '',
                "vehicleIdentificationCode" => '',
                "odometer" => [
                    "unit" => "Miles",
                    "currentValue" => $valuation->mileage
                ],
                "stockNumber" => $valuation->id ?? '',
            ];
        }
        return [];
    }

    /**
     * Get vehicle of interest data from enquiry source (vehicle / offer)
     *
     * @param EnquiryModel $enquiry
     * @return array
     */
    private function vehicleOfInterestData(EnquiryModel $enquiry): array
    {
        if ($enquiry->reason_type === 'vehicle') {
            /** @var Vehicle $vehicle */
            $vehicle = $enquiry->reason;
            return [
                "isUsed" => $vehicle->is_new ? 0 : 1,
                "price" => $vehicle->price ?? 0,
                "make" => $vehicle->make->name ?? '',
                "range" => '',
                "model" => $vehicle->model->name ?? '',
                "derivative" => $vehicle->derivative ?? '',
                "vrm" => $vehicle->registration_number ?? '',
                "fuelType" => $vehicle->fuelType->name ?? '',
                "vehicleIdentificationCode" => $vehicle->cap_id ?? '',
                "odometer" => [
                    "unit" => "Miles",
                    "currentValue" => $vehicle->odometer_mi
                ],
                "stockNumber" => $vehicle->id ?? ''
            ];
        }

        if ($enquiry->reason_type === 'offer') {
            /** @var VehicleOffer $offer */
            $offer = $enquiry->reason;
            return [
                "isUsed" => 0,
                "price" => $offer->price ?? 0,
                "make" => $offer->make->name ?? '',
                "range" => '',
                "model" => $offer->model->name ?? '',
                "derivative" => $offer->derivative ?? '',
                "vrm" => '',
                "fuelType" => '',
                "vehicleIdentificationCode" => '',
                "odometer" => [
                    "unit" => "Miles",
                    "currentValue" => 0
                ],
                "stockNumber" => $offer->id ?? ''
            ];
        }

        return [];
    }

    private function dealerIdByDistance(EnquiryModel $enquiry, EnquiryActionModel $action, $data = []): string|int
    {
        $dealer = '';

        // use requested dealership, if one has been set
        if (in_array($enquiry->reason_type, ['vehicle', 'offer'])) {
            $dealer = $enquiry->reason->dealership->data['enquiry-max-dealership'] ?? '';
        }

        // if no dealer, try to get closest dealer to submitted postcode
        if (empty($dealer)) {
            $closest_dealer = $this->getClosestDealershipToPostcode(
                $data['customer']['address']['postcode'],
                $this->getDealershipRecipientsForEnquiry($action)
            );

            $dealer = $closest_dealer ? $closest_dealer->data['enquiry-max-dealership'] : '';
        }

        // if still no dealer, fallback to any fallback value
        if (empty($dealer)) {
            $dealer = $action->data['fallback-dealership'] ?? '';
        }

        $match = $this->dealers
            ->filter(fn($entry) => $entry['dealerName'] === $dealer)
            ->first();

        return $match ? $match['dealerId'] : $this->dealers->first()['dealerId'];
    }

    private function getDealershipRecipientsForEnquiry(EnquiryActionModel $action): Collection
    {
        $dealership_query = Dealership::query();
        $dealer_id = $action->data['dealer-id'] ?? '';
        $franchise_id = $action->data['franchise-id'] ?? '';

        if ($dealer_id) {
            $dealership_query->where('id', '=', $dealer_id);
        } elseif ($franchise_id) {
            $dealership_query->where('franchise_id', '=', $franchise_id);
        }

        return $dealership_query->get();
    }

    private function getClosestDealershipToPostcode($postcode = '', $dealerships = []): ?Dealership
    {
        $locating_service = App::make(LocatingService::class);

        try {
            $postcode_lat_long = $locating_service->locate($postcode);
        } catch (\Exception) {
            Log::info('Postcode not found: ' . $postcode);
            return null;
        }

        if ($this->postcodeNotFound($postcode_lat_long)) {
            return null;
        }

        $closest_dealer_id = collect($dealerships)
            ->keyBy('id')
            ->map(fn($dealer) => explode(',', (string) $dealer->coordinates))
            ->filter(fn($coords) => count($coords) == 2)
            ->map(fn($coords) => $locating_service->getDistanceBetween(
                $postcode_lat_long->lat(),
                $postcode_lat_long->lng(),
                $coords[0],
                $coords[1],
            ))
            ->sort()
            ->keys()
            ->first();

        return Dealership::query()->where('id', '=', $closest_dealer_id)->first();
    }

    /**
     * Identify whether the postcode was not found.
     * Note that a lat long of 0,0 could be returned for postcodes which could not be found.
     *
     * @param $postcode_lat_long
     * @return bool
     */
    private function postcodeNotFound($postcode_lat_long): bool
    {
        return (
            empty($postcode_lat_long)
            || (
                empty($postcode_lat_long->lat())
                && empty($postcode_lat_long->lng())
            )
        );
    }

    private function offerSuffix(EnquiryModel $enquiry): string
    {
        if (Settings::get('crm-enquiry-max-append-offer-type-to-enquiry-title') !== true) {
            return '';
        }

        $offer_type = '';

        try {
            $offer_type = $enquiry->getNormalizedAnswers()
                ->filter(fn($answer) => $answer['question'] == 'Offer')
                ->map(fn($answer) => VehicleOffer::query()->find($answer['answer'])?->type)->first();
        } catch (\Exception $exception) {
            Log::error('Exception: ' . $exception->getMessage());
        } finally {
            return $offer_type
                ? ' - ' . $offer_type
                : '';
        }
    }
}
