<?php

namespace App\VehicleSpec\Services;

use App\Facades\Settings;
use App\VehicleSpec\Config\CapConfig;
use App\VehicleSpec\Contracts\VehicleSpecData;
use App\VehicleSpec\Contracts\VehicleSpecProvider;
use App\VehicleSpec\Contracts\VehicleSpecRequestData;
use App\VehicleSpec\Contracts\VehicleStandardEquipmentItem;
use App\VehicleSpec\Contracts\VehicleStandardEquipmentRequest;
use App\VehicleSpec\Contracts\VehicleTechnicalDataItem;
use App\VehicleSpec\Contracts\VehicleTechnicalDataRequest;
use App\VehicleType;
use Carbon\Carbon;
use DOMDocument;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Mtc\MercuryDataModels\ApiNotification;
use Mtc\MercuryDataModels\Vehicle;
use SimpleXMLElement;

/**
 * Note that this is a first draft proof of concept for a CAP service.
 * we may need multiple CAP services to integrate with their various APIs.
 */
class Cap implements VehicleSpecProvider
{
    private Vehicle $vehicle;

    public function __construct(
        protected readonly CapConfig $config
    ) {
        //
    }

    public function capIdLookup(Vehicle $vehicle): array
    {
        $this->vehicle = $vehicle;
        return match (Settings::get('vehicle-spec-providers-cap-default-lookup-attribute')) {
            'vrm' => !empty($vehicle->registration_number)
                ? $this->runLookup('vrm', $vehicle->registration_number)
                : $this->runLookup('vin', $vehicle->vin),
            default => !empty($vehicle->vin)
                ? $this->runLookup('vin', $vehicle->vin)
                : $this->runLookup('vrm', $vehicle->registration_number),
        };
    }

    /**
     * @param Vehicle $vehicle
     * @return VehicleSpecData
     */
    public function getSpec(Vehicle $vehicle): VehicleSpecData
    {
        $spec = new VehicleSpecData();
        $this->vehicle = $vehicle;

        try {
            $data = new VehicleSpecRequestData(
                $vehicle->registration_number ?? '',
                $vehicle->cap_id,
                $this->getVehicleType($vehicle),
            );

            $spec->technical_data = $this->getTechnicalData(new VehicleTechnicalDataRequest(
                $data->cap_id,
                $data->vehicle_type
            ));

            $spec->standard_equipment = $this->getStandardEquipment(new VehicleStandardEquipmentRequest(
                $data->cap_id,
                $data->vehicle_type
            ));

            if (empty($vehicle->model_id)) {
                $spec->basics = $this->getBasics($data->cap_id, $data->vehicle_type);
            }
        } catch (Exception $exception) {
            Log::error(__CLASS__ . '::' . __FUNCTION__ . '() ' . $exception->getMessage());
        } finally {
            return $spec;
        }
    }

    /**
     * @param VehicleStandardEquipmentRequest $request
     * @return Collection
     */
    public function getStandardEquipment(VehicleStandardEquipmentRequest $request): Collection
    {
        return $this->processStandardEquipmentResponse(
            $this->call('/GetStandardEquipment', $this->mapStandardEquipmentRequest($request))
        );
    }

    /**
     * @param VehicleTechnicalDataRequest $request
     * @return Collection
     */
    public function getTechnicalData(VehicleTechnicalDataRequest $request): Collection
    {
        return $this->processTechnicalDataResponse(
            $this->call('/GetTechnicalData', $this->mapTechnicalDataRequest($request))
        );
    }

    public function getBasics(string $cap_id, string $vehicle_type): Collection
    {
        return $this->processBasicDetails(
            $this->call('/GetCapDescriptionFromId', [
                'subscriberId' => $this->config->subscriberId(),
                'password' => $this->config->password(),
                'database' => $this->getCapDatabaseNameFromVehicleType($vehicle_type),
                'capid' => $cap_id,
            ], 'capvehicles')
        );
    }

    /**
     * @param VehicleSpecRequestData $data
     * @return array
     */
    protected function mapRequest(VehicleSpecRequestData $data): array
    {
        // TODO: build request data for integration, using the data passed in
        // we know the object type that we are receiving, so we should be able to take any fields that we need

        return [
            'subscriberId' => $this->config->subscriberId(),
            'password' => $this->config->password(),
            'database' => $this->getCapVehicleDatabase($data->vehicle_type),
            'capid' => $data->cap_id,
            'techDate' => $data->date,
            'justCurrent' => $data->just_current ? 'true' : 'false',
        ];
    }

    /**
     * @param string $vehicle_type
     * @return string
     */
    protected function getCapVehicleDatabase(string $vehicle_type)
    {
        // TODO: check if we can make a better, more globally reusable way to check vehicle type

        if (strtolower($vehicle_type) === 'lcv') {
            return CapVehicleDatabaseName::LCV->value;
        }

        // TODO: check if we should identify motorcycles, HGVs etc

        return CapVehicleDatabaseName::CAR->value;
    }

    /**
     * @param string $url
     * @param array $request_data
     * @return string
     */
    protected function call(string $url, array $request_data, string $type = 'nvd')
    {
        $response = Http::asForm()
            ->timeout(10)
            ->withOptions(['http_errors' => false])
            ->retry(1, random_int(250, 2500)) // retry once at random wait time of 250ms to 2.5s
            ->post($this->endpoint($url, $type), $request_data);

        ApiNotification::query()
            ->create([
                'provider' => 'cap',
                'data' => [
                    'request_data' => $request_data,
                    'endpoint' => $this->endpoint($url, $type),
                    'status_code' => $response->status(),
                    'response' => $response->body(),
                ],
                'processed' => $response->successful(),
                'data_model' => 'vehicle',
                'reference' => $this->vehicle?->vrm_condensed,
            ]);

        if ($response->failed()) {
            Log::error('Failed to call CAP ' . $url, [
                'request_data' => $request_data,
                'status_code' => $response->status(),
                'response' => $response->body(),
            ]);
            throw new Exception('Failed to perform call to ' . $url);
        }

        return $response->body();
    }

    /**
     * @param VehicleTechnicalDataRequest $data
     * @return array
     */
    protected function mapTechnicalDataRequest(VehicleTechnicalDataRequest $data): array
    {
        return [
            'subscriberId' => $this->config->subscriberId(),
            'password' => $this->config->password(),
            'database' => $this->getCapDatabaseNameFromVehicleType($data->vehicle_type),
            'capid' => $data->cap_id,
            'techDate' => ($data->date ?: '01-01-1990'),
            'justCurrent' => $data->just_current ? 'true' : 'false',
        ];
    }

    /**
     * @param string $xml
     * @return Collection
     * @throws Exception
     */
    protected function processTechnicalDataResponse(string $xml): Collection
    {
        $tech_data_array = $this->getXmlDataByElementName($xml, 'Tech');

        return collect($tech_data_array)->map(fn($item) => new VehicleTechnicalDataItem(
            $item->Tech_TechCode,
            $item->Dc_Description,
            $item->DT_LongDescription,
            $item->tech_value_string
        ));
    }

    protected function processBasicDetails(string $xml): Collection
    {
        $data = $this->getXmlDataByElementName($xml, 'Table');

        return collect([
            'make' => (string)$data[0]->CVehicle_ManText,
            'model' => (string)$data[0]->CVehicle_ShortModText,
        ]);
    }

    /**
     * @param VehicleStandardEquipmentRequest $data
     * @return array
     */
    protected function mapStandardEquipmentRequest(VehicleStandardEquipmentRequest $data): array
    {
        return [
            'subscriberId' => $this->config->subscriberId(),
            'password' => $this->config->password(),
            'database' => $this->getCapDatabaseNameFromVehicleType($data->vehicle_type),
            'capid' => $data->cap_id,
            'seDate' => ($data->date ?: '01-01-1990'),
            'justCurrent' => $data->just_current ? 'true' : 'false',
        ];
    }

    /**
     * @param string $xml
     * @return Collection
     * @throws Exception
     */
    protected function processStandardEquipmentResponse(string $xml): Collection
    {
        $standard_equipment_array = $this->getXmlDataByElementName($xml, 'SE');

        $data = collect($standard_equipment_array)->map(fn($item) => new VehicleStandardEquipmentItem(
            $item->Se_OptionCode,
            $item->Do_Description,
            $item->Dc_Description,
        ));

        return $data;
    }

    /**
     * Takes XML string and returns a boolean result where valid XML returns true
     *
     * @param string $xml
     * @return bool
     */
    private function isValidXml(string $xml): bool
    {
        libxml_use_internal_errors(true);
        libxml_clear_errors();
        $doc = new DOMDocument('1.0', 'utf-8');

        $doc->loadXML($xml);

        $errors = libxml_get_errors();

        return empty($errors);
    }

    /**
     * @param string $xml
     * @param string $element_name
     * @return array|false|SimpleXMLElement[]|null
     * @throws Exception
     */
    private function getXmlDataByElementName(string $xml, string $element_name)
    {
        if (!empty($xml) && $this->isValidXml($xml)) {
            $xml_element = new SimpleXMLElement($xml);
            $xml_element->registerXPathNamespace('d', 'urn:schemas-microsoft-com:xml-diffgram-v1');
            return $xml_element->xpath('//' . $element_name);
        }

        return [];
    }

    /**
     * @param string $path
     * @return string
     */
    private function endpoint(string $path, string $type = 'nvd'): string
    {
        return match ($type) {
            'capvehicles' => 'https://soap.cap.co.uk/vehicles/capvehicles.asmx/' . ltrim($path, '/'),
            'dvla' => 'https://soap.cap.co.uk/DVLA/CAPDVLA.asmx/' . ltrim($path, '/'),
            default => 'https://soap.cap.co.uk/Nvd/CapNvd.asmx/' . ltrim($path, '/'),
        };
    }

    /**
     * @param string $vehicle_type
     * @return string
     */
    private function getCapDatabaseNameFromVehicleType(string $vehicle_type): string
    {
        switch ($vehicle_type) {
            case VehicleType::CAR->value:
                return CapVehicleDatabaseName::CAR->value;
                break;
            case VehicleType::LCV->value:
                return CapVehicleDatabaseName::LCV->value;
                break;
            default:
                break;
        }

        return '';
    }

    private function runLookup(string $type, ?string $lookupCode): array
    {
        if (empty($lookupCode)) {
            return [];
        }

        $result = $this->call('/DVLALookup' . strtoupper($type), [
            'subscriberId' => $this->config->subscriberId(),
            'password' => $this->config->password(),
            $type => $lookupCode,
        ], 'dvla');

        $dvla = $this->getXmlDataByElementName($result, 'DVLA');
        $data = $this->getXmlDataByElementName($result, 'CAP');

        // not found
        if (empty($data[1])) {
            $result = [
                'cap_id' => null,
            ];
        } else {
            $result = [
                'type' => (string)$data[1]->VEHICLETYPE,
                'cap_id' => (string)$data[1]->CAPID,
                'cap_code' => (string)$data[1]->CAPCODE,
                'make_id' => (string)$data[1]->MANUFACTURER,
                'model_id' => (string)$data[1]->RANGE,
                'fuel_type_id' => (string)$data[1]->FUELTYPE,
                'transmission_id' => (string)$data[1]->TRANSMISSION,
                'door_count' => (int)$data[1]->DOORS,
            ];
        }

        $dvlaData = [];
        if (!empty($dvla[1]->REGISTRATIONDATE)) {
            $dvlaData = [
                'registration_number' => $dvla[1]->VRM,
                'vin' => $dvla[1]->VIN,
                'first_registration_date' => Carbon::createFromFormat('Ymd', (string)$dvla[1]->REGISTRATIONDATE),
                'make_id' => (string)$dvla[1]->MANUFACTURER,
                'model_id' => (string)$dvla[1]->MODEL,
                'door_count' => (int)$dvla[1]->DOORS,
                'seats' => (int)$dvla[1]->SEATING,
                'co2' => (int)$dvla[1]->CO2,
                'engine_size_cc' => (int)$dvla[1]->ENGINECAPACITY,
                'colour' => (int)$dvla[1]->COLOUR,
                'fuel_type_id' => (string)$dvla[1]->FUELTYPE,
                'manufacture_year' => !empty((string)$dvla[1]->MANUFACTUREDDATE)
                    ? Carbon::createFromFormat('Ymd', (string)$dvla[1]->MANUFACTUREDDATE)->format('Y')
                    : null,
                'body_style_id' => (string)$dvla[1]->BODYTYPE,
            ];
        }

        $overrideAttributes = Settings::get('vehicle-spec-providers-cap-override-attributes');
        if (!empty($overrideAttributes)) {
            foreach ($overrideAttributes as $overrideAttribute) {
                if ($overrideAttribute == 'model_to_derivative' && !empty($dvlaData['model_id'])) {
                    $dvlaData['derivative'] = $dvlaData['model_id'];
                } elseif (
                    $overrideAttribute == 'registration_date_to_year'
                    && !empty($dvlaData['first_registration_date'])
                ) {
                    $dvlaData['manufacture_year'] = $dvlaData['first_registration_date']->format('Y');
                }
            }
        }

        foreach ($dvlaData as $key => $entry) {
            if (empty($result[$key]) && !empty($entry)) {
                $result[$key] = $entry;
            }
        }

        return $result;
    }

    private function getVehicleType(Vehicle $vehicle): string
    {
        if (is_string($vehicle->type)) {
            return $vehicle->type;
        }

        if (is_object($vehicle->type) && get_class($vehicle->type) == VehicleType::class) {
            return $vehicle->type->value;
        }

        return 'unknown';
    }
}
