<?php

declare(strict_types=1);

namespace ColemanProjects\Taxi;

use ColemanProjects\Taxi\Validator\IntegerBetween;
use ColemanProjects\Taxi\Validator\IntegerGreaterThan;
use DateTimeImmutable;
use DateTimeInterface;
use Laminas\I18n\Validator\PostCode;
use Laminas\Validator\Date;
use Laminas\Validator\EmailAddress;
use Laminas\Validator\StringLength;
use PDO;
use Postmark\PostmarkClient;
use Symfony\Component\HttpFoundation\Request;

class BookingForm
{
    public const DATE_FORMAT_ISO = 'Y-m-d';
    public const DATE_FORMAT_UK = 'd/m/Y';
    public const TIME_FORMAT = 'H:i';

    public const NAME_MIN_LENGTH = 1;
    public const NAME_MAX_LENGTH = 255;

    public const POSTCODE_MIN_LENGTH = 0;
    public const POSTCODE_MAX_LENGTH = 50;

    public const PHONE_MIN_LENGTH = 10;
    public const PHONE_MAX_LENGTH = 30;

    public const COMPANY_MIN_LENGTH = 0;
    public const COMPANY_MAX_LENGTH = 255;

    public const STREET_MIN_LENGTH = 1;
    public const STREET_MAX_LENGTH = 255;

    public const LOCALITY_MIN_LENGTH = 0;
    public const LOCALITY_MAX_LENGTH = 255;

    public const TOWN_CITY_MIN_LENGTH = 1;
    public const TOWN_CITY_MAX_LENGTH = 255;

    public const SPECIAL_INSTRUCTIONS_MIN_LENGTH = 0;
    public const SPECIAL_INSTRUCTIONS_MAX_LENGTH = 2500;

    public const SEND_EMAIL_CUSTOMER_VENDOR = 1;

    public const POSTMARK_API_KEY = 'd9ff8986-d37e-4975-a48c-959ebc15cf5d';

    public const MYSQL_DATETIME_FORMAT = 'Y-m-d H:i:s';
    public const MAX_FORM_SUBMISSIONS = 3;
    public const FORM_SUBMISSION_PERIOD = '5 minutes';

    private $request;
    private $config;
    private $fields;
    private $booking;
    private $connection;

    public function __construct(Request $request, array $config)
    {
        $this->request = $request;
        $this->config = $config;

        $this->config['IS_DOCKER'] = isset($_ENV['DOCKER_RUNNING']);

        $dsn = 'mysql:host=' . $this->config['database']['hostname'];
        $dsn .= ';dbname=' . $this->config['database']['dbname'];
        $options = [
            PDO::ATTR_PERSISTENT => false,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
        ];
        $this->connection = new PDO(
            $dsn,
            $this->config['database']['username'],
            $this->config['database']['password'],
            $options
        );

        $this->fields = [
            'contact_name',
            'contact_email',
            'contact_telephone',
            'contact_cell_phone',
        ];

        if ($this->config['contact_company_name']) {
            $this->fields = array_merge($this->fields, [
                'contact_company_name'
            ]);
        }

        if ($this->config['outward_journey']) {
            $this->fields = array_merge($this->fields, [
                'outward_pick_up_date',
                'outward_pick_up_time_hour',
                'outward_pick_up_time_minute',
                'outward_pick_up_postcode',
                'outward_pick_up_street',
                'outward_pick_up_locality',
                'outward_pick_up_town_city',
                'outward_pick_up_special_instructions',
            ]);

            if ($this->config['outward_drop_off']) {
                $this->fields = array_merge($this->fields, [
                    'outward_drop_off_postcode',
                    'outward_drop_off_street',
                    'outward_drop_off_locality',
                    'outward_drop_off_town_city',
                ]);
            }
        }

        if ($this->config['passengers']) {
            $this->fields = array_merge($this->fields, [
                'passenger_count'
            ]);
        }

        if ($this->config['luggage']) {
            $this->fields = array_merge($this->fields, [
                'luggage_count'
            ]);
        }

        if ($this->config['taxis']) {
            $this->fields = array_merge($this->fields, [
                'taxi_count'
            ]);
        }

        if ($this->config['tour']) {
            $this->fields = array_merge($this->fields, [
                'tour'
            ]);
        }

        if ($this->config['guests']) {
            $this->fields = array_merge($this->fields, [
                'guest_count'
            ]);
        }

        if ($this->config['marshals']) {
            $this->fields = array_merge($this->fields, [
                'marshal_count'
            ]);
        }

        if ($this->config['taxi_model']) {
            $this->fields = array_merge($this->fields, [
                'taxi_model'
            ]);
        }

        if ($this->config['wedding_taxi']) {
            $this->fields = array_merge($this->fields, [
                'wedding_taxi_model'
            ]);
        }

        if ($this->config['booking_duration']) {
            $this->fields = array_merge($this->fields, [
                'booking_duration'
            ]);
        }

        if ($this->config['return_journey']) {
            $this->fields = array_merge($this->fields, [
                'reverse_outward_journey',
                'return_pick_up_date',
                'return_pick_up_time_hour',
                'return_pick_up_time_minute',
                'return_pick_up_postcode',
                'return_pick_up_street',
                'return_pick_up_locality',
                'return_pick_up_town_city',
                'return_drop_off_postcode',
                'return_drop_off_street',
                'return_drop_off_locality',
                'return_drop_off_town_city'
            ]);
        }

        if ($this->config['billing']) {
            $this->fields = array_merge($this->fields, [
                'billing_name',
                'billing_company_name',
                'billing_postcode',
                'billing_street',
                'billing_locality',
                'billing_town_city',
                'billing_country',
                'billing_state'
            ]);
        }

        $this->fields = array_merge($this->fields, [
            'how_find_us',
            'terms_and_conditions',
            'captcha_answer',
            'captcha_question'
        ]);

        if ($this->config['common_addresses']) {
            $this->fields = array_merge($this->fields, [
                'outward_pick_up_common_address',
                'outward_drop_off_common_address'
            ]);

            if ($this->config['return_journey']) {
                $this->fields = array_merge($this->fields, [
                    'return_pick_up_common_address',
                    'return_drop_off_common_address'
                ]);
            }
        }
    }

    public function generateTransactionId(): string
    {
        /** @todo Replace this with something that will work for more than one submission per second */
        return strval(time());
    }

    public function getFormPopulated(): array
    {
        $form = [];

        foreach ($this->fields as $field) {
            $form[$field] = $this->request->request->get($field, null);
        }

        return $form;
    }

    public function validate(): array
    {
        $errors = [];
        $form = $this->getFormPopulated();
        $booking = [];
        $tourOptions = $this->getTourOptions();
        $taxiModelOptions = $this->getTaxiModelOptions();
        $weddingTaxiModelOptions = $this->getWeddingTaxiModelOptions();
        $bookingDurationOptions = $this->getBookingDurationOptions();
        $countryOptions = $this->getCountryOptions();
        $stateOptions = $this->getStateOptions();
        $howFindUsOptions = $this->getHowFindUsOptions();
        $commonAddresses = $this->getCommonAddresses();

        $now = new DateTimeImmutable();

        if ($form['contact_name'] !== null) {
            $validator = new StringLength([
                'min' => self::NAME_MIN_LENGTH,
                'max' => self::NAME_MAX_LENGTH
            ]);

            if ($validator->isValid($form['contact_name'])) {
                $booking['contact_name'] = trim($form['contact_name']);
            } else {
                $errors['contact_name'] = 'Please provide a contact name';
            }
        } else {
            $errors['general'] = 'Please provide a contact name';
        }

        if ($this->config['contact_company_name']) {
            if ($form['contact_company_name'] !== null) {
                $validator = new StringLength([
                    'min' => self::NAME_MIN_LENGTH,
                    'max' => self::NAME_MAX_LENGTH
                ]);

                if ($validator->isValid($form['contact_company_name'])) {
                    $booking['contact_company_name'] = trim($form['contact_company_name']);
                } else {
                    $errors['contact_company_name'] = 'Please provide a company name';
                }
            } else {
                $errors['general'] = 'Please provide a company name';
            }
        }

        if ($form['contact_email'] !== null) {
            $validator = new EmailAddress();

            if ($validator->isValid($form['contact_email'])) {
                $booking['contact_email'] = $form['contact_email'];
            } else {
                $errors['contact_email'] = 'Please provide a valid contact email address';
            }
        } else {
            $errors['general'] = 'Please provide a contact email address';
        }

        if ($form['contact_telephone'] !== null) {
            $validator = new StringLength([
                'min' => self::PHONE_MIN_LENGTH,
                'max' => self::PHONE_MAX_LENGTH
            ]);

            if ($validator->isValid($form['contact_telephone'])) {
                $booking['contact_telephone'] = $form['contact_telephone'];
            } else {
                $errors['contact_telephone'] = 'Please provide a valid contact telephone number';
            }
        } else {
            $errors['general'] = 'Please provide a contact telephone number';
        }

        if ($form['contact_cell_phone'] !== null) {
            $validator = new StringLength([
                'min' => self::PHONE_MIN_LENGTH,
                'max' => self::PHONE_MAX_LENGTH
            ]);

            if ($validator->isValid($form['contact_cell_phone'])) {
                $booking['contact_cell_phone'] = $form['contact_cell_phone'];
            } else {
                $errors['contact_cell_phone'] = 'Please provide a valid contact mobile phone number';
            }
        } else {
            $errors['general'] = 'Please provide a contact mobile phone number';
        }

        if ($this->config['passengers']) {
            if ($form['passenger_count'] !== null) {
                $validator = new IntegerGreaterThan([
                    'min' => 1
                ]);

                if ($validator->isValid($form['passenger_count'])) {
                    $booking['passenger_count'] = (int) $form['passenger_count'];
                } else {
                    $errors['passenger_count'] = 'Please provide the number of passengers (at least 1)';
                }
            } else {
                $errors['general'] = 'Please provide the number of passengers';
            }
        }

        if ($this->config['luggage']) {
            if ($form['luggage_count'] !== null) {
                $validator = new IntegerGreaterThan([
                    'min' => 0
                ]);

                if ($validator->isValid($form['luggage_count'])) {
                    $booking['luggage_count'] = (int) $form['luggage_count'];
                } else {
                    $errors['luggage_count'] = 'Please provide the number of items of luggage (0 or more)';
                }
            } else {
                $errors['general'] = 'Please provide the number of items of luggage';
            }
        }

        if ($this->config['taxis']) {
            if ($form['taxi_count'] !== null) {
                $validator = new IntegerGreaterThan([
                    'min' => 1
                ]);

                if ($validator->isValid($form['taxi_count'])) {
                    $booking['taxi_count'] = (int) $form['taxi_count'];
                } else {
                    $errors['taxi_count'] = 'Please provide the number of taxis (at least 1)';
                }
            } else {
                $errors['general'] = 'Please provide the number of taxis';
            }
        }

        if ($this->config['tour']) {
            if ($form['tour'] !== null) {
                if (isset($tourOptions[$form['tour']])) {
                    $booking['tour'] = $form['tour'];
                } else {
                    $errors['tour'] = 'Please select one of the tour options';
                }
            } else {
                $errors['general'] = 'Please select a tour option';
            }
        }

        if ($this->config['guests']) {
            if ($form['guest_count'] !== null) {
                $validator = new IntegerBetween([
                    'min' => 100,
                    'max' => 1000
                ]);

                if ($validator->isValid($form['guest_count'])) {
                    $booking['guest_count'] = (int) $form['guest_count'];
                } else {
                    $errors['guest_count'] = 'Please provide the number of guests (between 100 and 1000)';
                }
            } else {
                $errors['general'] = 'Please provide the number of guests';
            }
        }

        if ($this->config['marshals']) {
            if ($form['marshal_count'] !== null) {
                $validator = new IntegerBetween([
                    'min' => 1,
                    'max' => 10
                ]);

                if ($validator->isValid($form['marshal_count'])) {
                    $booking['marshal_count'] = (int) $form['marshal_count'];
                } else {
                    $errors['marshal_count'] = 'Please provide the number of marshals (between 1 and 10)';
                }
            } else {
                $errors['general'] = 'Please provide the number of marshals';
            }
        }

        if ($this->config['taxi_model']) {
            if ($form['taxi_model'] !== null) {
                if (isset($taxiModelOptions[$form['taxi_model']])) {
                    $booking['taxi_model'] = $form['taxi_model'];
                } else {
                    $errors['taxi_model'] = 'Please select one of the taxi model options';
                }
            } else {
                $errors['general'] = 'Please select a taxi model option';
            }
        }

        if ($this->config['wedding_taxi']) {
            if ($form['wedding_taxi_model'] !== null) {
                if (isset($weddingTaxiModelOptions[$form['wedding_taxi_model']])) {
                    $booking['wedding_taxi_model'] = $form['wedding_taxi_model'];
                } else {
                    $errors['wedding_taxi_model'] = 'Please select one of the taxi model options';
                }
            } else {
                $errors['general'] = 'Please select a taxi model option';
            }
        }

        if ($this->config['booking_duration']) {
            if ($form['booking_duration'] !== null) {
                if (isset($bookingDurationOptions[$form['booking_duration']])) {
                    $booking['booking_duration'] = $form['booking_duration'];
                } else {
                    $errors['booking_duration'] = 'Please select one of the booking duration options';
                }
            } else {
                $errors['general'] = 'Please select a booking duration option';
            }
        }

        if ($this->config['outward_journey']) {
            // Pick Up address - always in UK
            if ($form['outward_pick_up_date'] !== null) {
                $validator = new Date([
                    'format' => self::DATE_FORMAT_ISO,
                    'strict' => true
                ]);

                if ($validator->isValid($form['outward_pick_up_date'])) {
                    $booking['outward_pick_up_date'] = $form['outward_pick_up_date'];
                } else {
                    // If we don't have an ISO date, the user might have a browser which
                    // doesn't support the input type 'date', in which case expect a UK date
                    $validator = new Date([
                        'format' => self::DATE_FORMAT_UK,
                        'strict' => true
                    ]);

                    if ($validator->isValid($form['outward_pick_up_date'])) {
                        $booking['outward_pick_up_date'] = DateTimeImmutable::createFromFormat(self::DATE_FORMAT_UK, $form['outward_pick_up_date'])->format(self::DATE_FORMAT_ISO);
                    } else {
                        $errors['outward_pick_up_date'] = 'Please enter a';
                        $errors['outward_pick_up_date'] .= $this->config['event'] ? 'n event' : ' pick up';
                        $errors['outward_pick_up_date'] .= ' date: dd/mm/yyyy';
                    }
                }
            } else {
                $errors['general'] = 'Please enter a';
                $errors['general'] .= $this->config['event'] ? 'n event' : ' pick up';
                $errors['outward_pick_up_date'] .= ' date: dd/mm/yyyy';
            }

            if ($form['outward_pick_up_time_hour'] !== null) {
                $validator = new IntegerBetween([
                    'min' => 0,
                    'max' => 23
                ]);

                if ($validator->isValid($form['outward_pick_up_time_hour'])) {
                    $booking['outward_pick_up_time_hour'] = $form['outward_pick_up_time_hour'];
                } else {
                    $errors['outward_pick_up_time_hour'] = 'Please enter a';
                    $errors['outward_pick_up_time_hour'] .= $this->config['event'] ? 'n event' : ' pick up';
                    $errors['outward_pick_up_time_hour'] .= ' time (24 hour format)';
                }
            } else {
                $errors['general'] = 'Please enter a';
                $errors['general'] .= $this->config['event'] ? 'n event' : ' pick up';
                $errors['general'] .= ' time';
            }

            if ($form['outward_pick_up_time_minute'] !== null) {
                $validator = new IntegerBetween([
                    'min' => 0,
                    'max' => 59
                ]);

                if ($validator->isValid($form['outward_pick_up_time_minute'])) {
                    $booking['outward_pick_up_time_minute'] = $form['outward_pick_up_time_minute'];
                } else {
                    $errors['outward_pick_up_time_minute'] = 'Please enter a';
                    $errors['outward_pick_up_time_minute'] .= $this->config['event'] ? 'n event' : ' pick up';
                    $errors['outward_pick_up_time_minute'] .= ' time (24 hour format)';
                }
            } else {
                $errors['general'] = 'Please enter a';
                $errors['general'] .= $this->config['event'] ? 'n event' : ' pick up';
                $errors['general'] .= ' time';
            }

            if ($this->config['common_addresses']) {
                if ($form['outward_pick_up_common_address'] !== null) {
                    $booking['outward_pick_up_common_address_name'] = isset($commonAddresses[$form['outward_pick_up_common_address']]) ? $commonAddresses[$form['outward_pick_up_common_address']]['name'] : '';
                }
            }

            if ($form['outward_pick_up_postcode'] !== null) {
                $validator = new StringLength([
                    'min' => self::POSTCODE_MIN_LENGTH,
                    'max' => self::POSTCODE_MAX_LENGTH
                ]);

                if ($validator->isValid($form['outward_pick_up_postcode'])) {
                    $booking['outward_pick_up_postcode'] = $form['outward_pick_up_postcode'];
                } else {
                    $errors['outward_pick_up_postcode'] = 'Please enter a ';
                    $errors['outward_pick_up_postcode'] .= $this->config['event'] ? 'event' : ' pick up';
                    $errors['outward_pick_up_postcode'] .= ' postcode between ' . self::POSTCODE_MIN_LENGTH . ' and ' . self::POSTCODE_MAX_LENGTH . ' characters';
                }
            } else {
                $errors['general'] = 'Please enter a valid ';
                $errors['general'] .= $this->config['event'] ? 'event' : ' pick up';
                $errors['general'] .= ' postcode';
            }

            if ($form['outward_pick_up_street'] !== null) {
                $validator = new StringLength([
                    'min' => self::STREET_MIN_LENGTH,
                    'max' => self::STREET_MAX_LENGTH
                ]);

                if ($validator->isValid($form['outward_pick_up_street'])) {
                    $booking['outward_pick_up_street'] = $form['outward_pick_up_street'];
                } else {
                    $errors['outward_pick_up_street'] = 'Please enter a valid address 1';
                }
            } else {
                $errors['general'] = 'Please enter a valid address 1';
            }

            if ($form['outward_pick_up_locality'] !== null) {
                $validator = new StringLength([
                    'min' => self::LOCALITY_MIN_LENGTH,
                    'max' => self::LOCALITY_MAX_LENGTH
                ]);

                if ($validator->isValid($form['outward_pick_up_locality'])) {
                    $booking['outward_pick_up_locality'] = $form['outward_pick_up_locality'];
                } else {
                    $errors['outward_pick_up_locality'] = 'Please enter a valid address 2';
                }
            } else {
                $errors['general'] = 'Please enter a valid address 2';
            }

            if ($form['outward_pick_up_town_city'] !== null) {
                $validator = new StringLength([
                    'min' => self::TOWN_CITY_MIN_LENGTH,
                    'max' => self::TOWN_CITY_MAX_LENGTH
                ]);

                if ($validator->isValid($form['outward_pick_up_town_city'])) {
                    $booking['outward_pick_up_town_city'] = $form['outward_pick_up_town_city'];
                } else {
                    $errors['outward_pick_up_town_city'] = 'Please enter a valid town/city';
                }
            } else {
                $errors['general'] = 'Please enter a valid town/city';
            }

            if ($form['outward_pick_up_special_instructions'] !== null) {
                $validator = new StringLength([
                    'min' => self::SPECIAL_INSTRUCTIONS_MIN_LENGTH,
                    'max' => self::SPECIAL_INSTRUCTIONS_MAX_LENGTH
                ]);

                if ($validator->isValid($form['outward_pick_up_special_instructions'])) {
                    $booking['outward_pick_up_special_instructions'] = $form['outward_pick_up_special_instructions'];
                } else {
                    $errors['outward_pick_up_special_instructions'] = 'Please provide special instructions';
                }
            } else {
                $errors['general'] = 'Please provide special instructions';
            }

            if ($this->config['outward_drop_off']) {
                if ($this->config['common_addresses']) {
                    if ($form['outward_drop_off_common_address'] !== null) {
                        $booking['outward_drop_off_common_address_name'] = isset($commonAddresses[$form['outward_drop_off_common_address']]) ? $commonAddresses[$form['outward_drop_off_common_address']]['name'] : '';
                    }
                }

                // Drop Off address - always in UK
                if ($form['outward_drop_off_postcode'] !== null) {
                    $validator = new StringLength([
                        'min' => self::POSTCODE_MIN_LENGTH,
                        'max' => self::POSTCODE_MAX_LENGTH
                    ]);

                    if ($validator->isValid($form['outward_drop_off_postcode'])) {
                        $booking['outward_drop_off_postcode'] = $form['outward_drop_off_postcode'];
                    } else {
                        $errors['outward_drop_off_postcode'] = 'Please enter an outward drop off postcode between ' . self::POSTCODE_MIN_LENGTH . ' and ' . self::POSTCODE_MAX_LENGTH . ' characters';
                    }
                } else {
                    $errors['general'] = 'Please enter an outward drop off postcode';
                }

                if ($form['outward_drop_off_street'] !== null) {
                    $validator = new StringLength([
                        'min' => self::STREET_MIN_LENGTH,
                        'max' => self::STREET_MAX_LENGTH
                    ]);

                    if ($validator->isValid($form['outward_drop_off_street'])) {
                        $booking['outward_drop_off_street'] = $form['outward_drop_off_street'];
                    } else {
                        $errors['outward_drop_off_street'] = 'Please enter a valid address 1';
                    }
                } else {
                    $errors['general'] = 'Please enter an outward drop off address 1';
                }

                if ($form['outward_drop_off_locality'] !== null) {
                    $validator = new StringLength([
                        'min' => self::LOCALITY_MIN_LENGTH,
                        'max' => self::LOCALITY_MAX_LENGTH
                    ]);

                    if ($validator->isValid($form['outward_drop_off_locality'])) {
                        $booking['outward_drop_off_locality'] = $form['outward_drop_off_locality'];
                    } else {
                        $errors['outward_drop_off_locality'] = 'Please enter a valid address 2';
                    }
                } else {
                    $errors['general'] = 'Please enter an outward drop off address 2';
                }

                if ($form['outward_drop_off_town_city'] !== null) {
                    $validator = new StringLength([
                        'min' => self::TOWN_CITY_MIN_LENGTH,
                        'max' => self::TOWN_CITY_MAX_LENGTH
                    ]);

                    if ($validator->isValid($form['outward_drop_off_town_city'])) {
                        $booking['outward_drop_off_town_city'] = $form['outward_drop_off_town_city'];
                    } else {
                        $errors['outward_drop_off_town_city'] = 'Please enter a valid town/city';
                    }
                } else {
                    $errors['general'] = 'Please enter an outward drop off town/city';
                }
            }
        }

        if ($this->config['return_journey']) {
            // A return is required if a pick up date is specified
            $returnRequired = $form['reverse_outward_journey'] || ($form['return_pick_up_date'] !== null && strlen($form['return_pick_up_date']) >= 1);

            // Pick Up address - always in UK
            if ($form['return_pick_up_date'] !== null) {
                if ($returnRequired) {
                    $validator = new Date([
                        'format' => self::DATE_FORMAT_ISO,
                        'strict' => true
                    ]);

                    if ($validator->isValid($form['return_pick_up_date'])) {
                        $booking['return_pick_up_date'] = $form['return_pick_up_date'];
                    } else {
                        // If we don't have an ISO date, the user might have a browser which
                        // doesn't support the input type 'date', in which case expect a UK date
                        $validator = new Date([
                            'format' => self::DATE_FORMAT_UK,
                            'strict' => true
                        ]);

                        if ($validator->isValid($form['return_pick_up_date'])) {
                            $booking['return_pick_up_date'] = DateTimeImmutable::createFromFormat(self::DATE_FORMAT_UK, $form['return_pick_up_date'])->format(self::DATE_FORMAT_ISO);
                        } else {
                            $errors['return_pick_up_date'] = 'Please enter a return pick up date: dd/mm/yyyy';
                        }
                    }
                }
            } else {
                $errors['general'] = 'Please enter a return pick up date';
            }

            if ($form['return_pick_up_time_hour'] !== null) {
                if ($returnRequired || strlen($form['return_pick_up_time_hour']) >= 1) {
                    $validator = new IntegerBetween([
                        'min' => 0,
                        'max' => 23
                    ]);

                    if ($validator->isValid($form['return_pick_up_time_hour'])) {
                        $booking['return_pick_up_time_hour'] = $form['return_pick_up_time_hour'];
                    } else {
                        $errors['return_pick_up_time_hour'] = 'Please enter a return pick up time (24 hour format)';
                    }
                }
            } else {
                $errors['general'] = 'Please enter a return pick up time';
            }

            if ($form['return_pick_up_time_minute'] !== null) {
                if ($returnRequired || strlen($form['return_pick_up_time_minute']) >= 1) {
                    $validator = new IntegerBetween([
                        'min' => 0,
                        'max' => 59
                    ]);

                    if ($validator->isValid($form['return_pick_up_time_minute'])) {
                        $booking['return_pick_up_time_minute'] = $form['return_pick_up_time_minute'];
                    } else {
                        $errors['return_pick_up_time_minute'] = 'Please enter a return pick up time (24 hour format)';
                    }
                }
            } else {
                $errors['general'] = 'Please enter a return pick up time';
            }

            if ($this->config['common_addresses']) {
                if ($form['return_pick_up_common_address'] !== null) {
                    $booking['return_pick_up_common_address_name'] = isset($commonAddresses[$form['return_pick_up_common_address']]) ? $commonAddresses[$form['return_pick_up_common_address']]['name'] : '';
                }
            }

            if ($form['return_pick_up_postcode'] !== null) {
                if ($returnRequired || strlen($form['return_pick_up_postcode']) >= 1) {
                    $validator = new StringLength([
                        'min' => self::POSTCODE_MIN_LENGTH,
                        'max' => self::POSTCODE_MAX_LENGTH
                    ]);

                    if ($validator->isValid($form['return_pick_up_postcode'])) {
                        $booking['return_pick_up_postcode'] = $form['return_pick_up_postcode'];
                    } else {
                        $errors['return_pick_up_postcode'] = 'Please enter a return pick up postcode between ' . self::POSTCODE_MIN_LENGTH . ' and ' . self::PHONE_MAX_LENGTH . ' characters';
                    }
                }
            } else {
                $errors['general'] = 'Please enter a return pick up postcode';
            }

            if ($form['return_pick_up_street'] !== null) {
                if ($returnRequired || strlen($form['return_pick_up_street']) >= 1) {
                    $validator = new StringLength([
                        'min' => self::STREET_MIN_LENGTH,
                        'max' => self::STREET_MAX_LENGTH
                    ]);

                    if ($validator->isValid($form['return_pick_up_street'])) {
                        $booking['return_pick_up_street'] = $form['return_pick_up_street'];
                    } else {
                        $errors['return_pick_up_street'] = 'Please enter a valid address 1';
                    }
                }
            } else {
                $errors['general'] = 'Please enter a return pick up address 1';
            }

            if ($form['return_pick_up_locality'] !== null) {
                if ($returnRequired || strlen($form['return_pick_up_locality']) >= 1) {
                    $validator = new StringLength([
                        'min' => self::LOCALITY_MIN_LENGTH,
                        'max' => self::LOCALITY_MAX_LENGTH
                    ]);

                    if ($validator->isValid($form['return_pick_up_locality'])) {
                        $booking['return_pick_up_locality'] = $form['return_pick_up_locality'];
                    } else {
                        $errors['return_pick_up_locality'] = 'Please enter a valid address 2';
                    }
                }
            } else {
                $errors['general'] = 'Please enter a return pick up address 2';
            }

            if ($form['return_pick_up_town_city'] !== null) {
                if ($returnRequired || strlen($form['return_pick_up_town_city']) >= 1) {
                    $validator = new StringLength([
                        'min' => self::TOWN_CITY_MIN_LENGTH,
                        'max' => self::TOWN_CITY_MAX_LENGTH
                    ]);

                    if ($validator->isValid($form['return_pick_up_town_city'])) {
                        $booking['return_pick_up_town_city'] = $form['return_pick_up_town_city'];
                    } else {
                        $errors['return_pick_up_town_city'] = 'Please enter a valid town/city';
                    }
                }
            } else {
                $errors['general'] = 'Please enter a return pick up town/city';
            }

            // Drop Off address - always in UK
            if ($this->config['common_addresses']) {
                if ($form['return_drop_off_common_address'] !== null) {
                    $booking['return_drop_off_common_address_name'] = isset($commonAddresses[$form['return_drop_off_common_address']]) ? $commonAddresses[$form['return_drop_off_common_address']]['name'] : '';
                }
            }

            if ($form['return_drop_off_postcode'] !== null) {
                if ($returnRequired || strlen($form['return_drop_off_postcode']) >= 1) {
                    $validator = new StringLength([
                        'min' => self::POSTCODE_MIN_LENGTH,
                        'max' => self::POSTCODE_MAX_LENGTH
                    ]);

                    if ($validator->isValid($form['return_drop_off_postcode'])) {
                        $booking['return_drop_off_postcode'] = $form['return_drop_off_postcode'];
                    } else {
                        $errors['return_drop_off_postcode'] = 'Please enter a return drop off postcode between ' . self::POSTCODE_MIN_LENGTH . ' and ' . self::POSTCODE_MAX_LENGTH . ' characters';
                    }
                }
            } else {
                $errors['general'] = 'Please enter a return pick up postcode';
            }

            if ($form['return_drop_off_street'] !== null) {
                if ($returnRequired || strlen($form['return_drop_off_street']) >= 1) {
                    $validator = new StringLength([
                        'min' => self::STREET_MIN_LENGTH,
                        'max' => self::STREET_MAX_LENGTH
                    ]);

                    if ($validator->isValid($form['return_drop_off_street'])) {
                        $booking['return_drop_off_street'] = $form['return_drop_off_street'];
                    } else {
                        $errors['return_drop_off_street'] = 'Please enter a valid address 1';
                    }
                }
            } else {
                $errors['general'] = 'Please enter a return drop off address 1';
            }

            if ($form['return_drop_off_locality'] !== null) {
                if ($returnRequired || strlen($form['return_drop_off_locality']) >= 1) {
                    $validator = new StringLength([
                        'min' => self::LOCALITY_MIN_LENGTH,
                        'max' => self::LOCALITY_MAX_LENGTH
                    ]);

                    if ($validator->isValid($form['return_drop_off_locality'])) {
                        $booking['return_drop_off_locality'] = $form['return_drop_off_locality'];
                    } else {
                        $errors['return_drop_off_locality'] = 'Please enter a valid address 2';
                    }
                }
            } else {
                $errors['general'] = 'Please enter a return drop off address 2';
            }

            if ($form['return_drop_off_town_city'] !== null) {
                if ($returnRequired || strlen($form['return_drop_off_town_city']) >= 1) {
                    $validator = new StringLength([
                        'min' => self::TOWN_CITY_MIN_LENGTH,
                        'max' => self::TOWN_CITY_MAX_LENGTH
                    ]);

                    if ($validator->isValid($form['return_drop_off_town_city'])) {
                        $booking['return_drop_off_town_city'] = $form['return_drop_off_town_city'];
                    } else {
                        $errors['return_drop_off_town_city'] = 'Please enter a valid town/city';
                    }
                }
            } else {
                $errors['general'] = 'Please enter a return pick up town/city';
            }
        }

        if ($this->config['billing']) {
            if ($form['billing_name'] !== null) {
                $validator = new StringLength([
                    'min' => self::NAME_MIN_LENGTH,
                    'max' => self::NAME_MAX_LENGTH
                ]);

                if ($validator->isValid($form['billing_name'])) {
                    $booking['billing_name'] = trim($form['billing_name']);
                } else {
                    $errors['billing_name'] = 'Please provide a billing name';
                }
            } else {
                $errors['billing_name'] = 'Please provide a billing name';
            }

            if ($form['billing_company_name'] !== null) {
                $validator = new StringLength([
                    'min' => self::COMPANY_MIN_LENGTH,
                    'max' => self::COMPANY_MAX_LENGTH
                ]);

                if ($validator->isValid($form['billing_company_name'])) {
                    $booking['billing_company_name'] = trim($form['billing_company_name']);
                } else {
                    $errors['billing_company_name'] = 'Please provide a billing company name';
                }
            } else {
                $errors['billing_company_name'] = 'Please provide a billing company name';
            }

            // Billing address - may not be in UK
            if ($form['billing_postcode'] !== null) {
                // Only validate billing postcode if country is GB/UK
                if ($form['billing_country'] !== null && $form['billing_country'] == 'GB') {
                    $validator = new PostCode([
                        'locale' => 'en_GB'
                    ]);

                    if ($validator->isValid(strtoupper($form['billing_postcode']))) {
                        $booking['billing_postcode'] = strtoupper($form['billing_postcode']);
                    } else {
                        $errors['billing_postcode'] = 'Please enter a valid billing postcode';
                    }
                } else {
                    $validator = new StringLength([
                        'min' => 1,
                        'max' => 20
                    ]);

                    if ($validator->isValid($form['billing_postcode'])) {
                        $booking['billing_postcode'] = $form['billing_postcode'];
                    } else {
                        $errors['billing_postcode'] = 'Please enter a valid billing postcode';
                    }
                }
            } else {
                $errors['general'] = 'Please enter an outward pick up postcode';
            }

            if ($form['billing_street'] !== null) {
                $validator = new StringLength([
                    'min' => self::STREET_MIN_LENGTH,
                    'max' => self::STREET_MAX_LENGTH
                ]);

                if ($validator->isValid($form['billing_street'])) {
                    $booking['billing_street'] = $form['billing_street'];
                } else {
                    $errors['billing_street'] = 'Please enter a valid address 1';
                }
            } else {
                $errors['general'] = 'Please enter a billing address 1';
            }

            if ($form['billing_locality'] !== null) {
                $validator = new StringLength([
                    'min' => self::LOCALITY_MIN_LENGTH,
                    'max' => self::LOCALITY_MAX_LENGTH
                ]);

                if ($validator->isValid($form['billing_locality'])) {
                    $booking['billing_locality'] = $form['billing_locality'];
                } else {
                    $errors['billing_locality'] = 'Please enter a valid address 2';
                }
            } else {
                $errors['general'] = 'Please enter a billing address 2';
            }

            if ($form['billing_town_city'] !== null) {
                $validator = new StringLength([
                    'min' => self::TOWN_CITY_MIN_LENGTH,
                    'max' => self::TOWN_CITY_MAX_LENGTH
                ]);

                if ($validator->isValid($form['billing_town_city'])) {
                    $booking['billing_town_city'] = $form['billing_town_city'];
                } else {
                    $errors['billing_town_city'] = 'Please enter a valid town/city';
                }
            } else {
                $errors['general'] = 'Please enter a billing town/city';
            }

            if ($form['billing_country'] !== null) {
                if (isset($countryOptions[$form['billing_country']])) {
                    $booking['billing_country'] = $form['billing_country'];
                } else {
                    $errors['billing_country'] = 'Please select one of the options';
                }
            } else {
                $errors['general'] = 'Please select a billing country';
            }

            // Billing State is only required for US
            if (isset($booking['billing_country']) && $booking['billing_country'] === 'US') {
                if ($form['billing_state'] !== null) {
                    if (isset($stateOptions[$form['billing_state']])) {
                        $booking['billing_state'] = $form['billing_state'];
                    } else {
                        $errors['billing_state'] = 'Please select one of the options';
                    }
                } else {
                    $errors['general'] = 'Please select a billing state';
                }
            }
        }

        if ($form['how_find_us'] !== null) {
            if (isset($howFindUsOptions[$form['how_find_us']])) {
                $booking['how_find_us'] = $form['how_find_us'];
            } else {
                $errors['how_find_us'] = 'Please select one of the options';
            }
        } else {
            $errors['general'] = 'Please select a How did you find us option';
        }

        if ($form['terms_and_conditions'] !== null) {
            $booking['terms_and_conditions'] = 1;
        } else {
            $errors['terms_and_conditions'] = 'Please confirm that you agree to the full terms and conditions';
        }

        if ($form['captcha_question'] !== null && $form['captcha_answer'] !== null) {
            $question_parts = explode(',', $form['captcha_question']);

            if (count($question_parts) == 2 && is_numeric($question_parts[0]) && is_numeric($question_parts[1]) && is_numeric($form['captcha_answer'])) {
                if ($form['captcha_answer'] == ($question_parts[0] + $question_parts[1])) {
                    // CAPTCHA valid, no action
                } else {
                    $errors['captcha_answer'] = 'You must answer the arithmetic question correctly';
                }
            } else {
                $errors['captcha_answer'] = 'You must answer the arithmetic question correctly';
            }
        } else {
            $errors['captcha_answer'] = 'You must answer the arithmetic question correctly';
        }

        // Interdependent validation, e.g. date checks
        if (isset($booking['outward_pick_up_date']) && isset($booking['outward_pick_up_time_hour']) && isset($booking['outward_pick_up_time_minute'])) {
            $nextDaySubmissionCutOff = (new DateTimeImmutable())->setTime(21, 0, 0, 0);
            $nextDayPickUpCutOff = (new DateTimeImmutable('tomorrow'))->setTime(10, 0, 0, 0);

            $sameDaySubmissionCutOff = (new DateTimeImmutable())->setTime(10, 0, 0, 0);
            $sameDayPickUpCutOff = (new DateTimeImmutable())->setTime(10, 0, 0, 0);

            $outwardHour = str_pad($booking['outward_pick_up_time_hour'], 2, '0', STR_PAD_LEFT);
            $outwardMinute = str_pad($booking['outward_pick_up_time_minute'], 2, '0', STR_PAD_LEFT);
            $outwardPickUp = new DateTimeImmutable("{$booking['outward_pick_up_date']} $outwardHour:$outwardMinute:00");

            if ($outwardPickUp < $now) {
                $errors['outward_pick_up_date'] = 'Must be in the future';
                $errors['outward_pick_up_time_hour'] = 'Must be in the future';
                $errors['outward_pick_up_time_minute'] = 'Must be in the future';
            }

            if ($now > $nextDaySubmissionCutOff && $outwardPickUp < $nextDayPickUpCutOff) {
                $errors['general'] = 'Please do not submit this form after 21.00pm (UK Time) if you are requesting a booking before 10.00am the next morning';
                $errors['outward_pick_up_date'] = 'Must be after 10:00 tomorrow';
                $errors['outward_pick_up_time_hour'] = 'Must be after 10:00 tomorrow';
                $errors['outward_pick_up_time_minute'] = 'Must be after 10:00 tomorrow';
            }

            if ($now < $sameDaySubmissionCutOff && $outwardPickUp < $sameDayPickUpCutOff) {
                $errors['general'] = 'Please do not submit this form before 10.00am (UK Time) if you are requesting a booking before 10.00am the same morning';
                $errors['outward_pick_up_date'] = 'Must be after 10:00 today';
                $errors['outward_pick_up_time_hour'] = 'Must be after 10:00 today';
                $errors['outward_pick_up_time_minute'] = 'Must be after 10:00 today';
            }

            if (isset($booking['return_pick_up_date']) && isset($booking['return_pick_up_time_hour']) && isset($booking['return_pick_up_time_minute'])) {
                $returnHour = str_pad($booking['return_pick_up_time_hour'], 2, '0', STR_PAD_LEFT);
                $returnMinute = str_pad($booking['return_pick_up_time_minute'], 2, '0', STR_PAD_LEFT);
                $returnPickUp = new DateTimeImmutable("{$booking['return_pick_up_date']} $returnHour:$returnMinute:00");

                if ($returnPickUp <= $outwardPickUp) {
                    $errors['return_pick_up_date'] = 'Must be after outward journey date/time';
                    $errors['return_pick_up_time_hour'] = 'Must be after outward journey date/time';
                    $errors['return_pick_up_time_minute'] = 'Must be after outward journey date/time';
                }
            }
        }

        // Only log IP address and check submission count if data is valid
        if (count($errors) === 0) {
            if ($this->countPreviousSubmissions($_SERVER['REMOTE_ADDR']) >= self::MAX_FORM_SUBMISSIONS) {
                $errors['general'] = 'You have already submitted this form ';
                $errors['general'] .= self::MAX_FORM_SUBMISSIONS;
                $errors['general'] .= ' times, please wait ';
                $errors['general'] .= self::FORM_SUBMISSION_PERIOD;
                $errors['general'] .= ' before trying again';
            }

            $this->logSubmission($_SERVER['REMOTE_ADDR']);
        }

        if (count($errors) === 0) {
            $this->booking = $booking;
        }

        return $errors;
    }

    public function sendEmail(\Twig\Environment $template): void
    {
        $subject = $this->config['website'];
        $subject .= ' ';
        $subject .= $this->config['booking'] ? 'Booking' : 'Quotation';
        $subject .= ' Form';

        $body = $template->render('email/booking.twig.html', [
            'config' => $this->config,
            'booking' => $this->booking,
            'tourOptions' => $this->getTourOptions(),
            'taxiModelOptions' => $this->getTaxiModelOptions(),
            'weddingTaxiModelOptions' => $this->getWeddingTaxiModelOptions(),
            'bookingDurationOptions' => $this->getBookingDurationOptions(),
            'howFindUsOptions' => $this->getHowFindUsOptions()
        ]);

        $client = new PostmarkClient(self::POSTMARK_API_KEY);
        $htmlBody = null;
        $textBody = $body;
        $tag = $this->booking ? 'booking-form' : 'quotation-form';
        $messageStream = 'outbound';

        if (!$this->config['IS_DOCKER']) {
            // Send email to the customer
            $client->sendEmail(
                $this->config['website'] . ' <' . $this->config['EMAIL_FROM'] . '>',
                $this->booking['contact_email'],
                $subject,
                $htmlBody,
                $textBody,
                $tag,
                null, // Track Opens
                null, // Reply To
                null, // CC
                null, // BCC
                null, // Headers
                null, // Attachments
                null, // Track Links
                null, // Metadata
                $messageStream
            );

            // Send copy to the vendor
            $client->sendEmail(
                $this->booking['contact_email'] . '<' . $this->config['EMAIL_FROM'] . '>',
                $this->config['EMAIL_FROM'],
                $subject,
                $htmlBody,
                $textBody,
                $tag,
                null, // Track Opens
                $this->booking['contact_email'], // Reply To
                null, // CC
                null, // BCC
                null, // Headers
                null, // Attachments
                null, // Track Links
                null, // Metadata
                $messageStream
            );
        }
    }

    private function collapseWhitespace(string $str): string
    {
        return preg_replace('/\s+/', ' ', $str);
    }

    private function splitName(string $fullName): array
    {
        $fullName = trim($fullName);
        $fullName = $this->collapseWhitespace($fullName);

        $splitName = explode(' ', $fullName);
        $firstName = '';
        $lastName = '';

        if (count($splitName) === 1) {
            // Special case of when the user only has one part in their name
            $firstName = 'Unknown';
            $lastName = $splitName[0];
        } else {
            // Last name is the final part of the name
            $lastName = $splitName[count($splitName) - 1];

            // First name is all the other parts of the name
            for ($n = 0; $n < count($splitName) - 1; $n++) {
                $firstName .= $splitName[$n];
            }
        }

        return [
            'firstName' => $firstName,
            'lastName' => $lastName
        ];
    }

    public function getPaymentUrl(string $returnUrlPrefix): ?string
    {
        $opayo = new Opayo($this->config);

        $billingName = $this->splitName($this->booking['billing_name']);

        $data = [
            'VendorTxCode' => $this->generateTransactionId(),
            'Amount' => $this->config['deposit'],
            'Currency' => 'GBP',
            'Description' => 'Deposit for ' . $this->config['website'],
            'SuccessURL' => $returnUrlPrefix . 'complete.php',
            'FailureURL' => $returnUrlPrefix . 'failed.php',
            'CustomerName' => $this->booking['contact_name'],
            'CustomerEMail' => $this->booking['contact_email'],
            'VendorEMail' => $this->config['EMAIL_FROM'],
            'SendEMail' => self::SEND_EMAIL_CUSTOMER_VENDOR,
            'BillingSurname' => $billingName['lastName'],
            'BillingFirstnames' => $billingName['firstName'],
            'BillingAddress1' => $this->booking['billing_street'],
            'BillingAddress2' => $this->booking['billing_locality'],
            'BillingCity' => $this->booking['billing_town_city'],
            'BillingPostCode' => $this->booking['billing_postcode'],
            'BillingCountry' => $this->booking['billing_country'],
            'BillingPhone' => $this->booking['contact_cell_phone'],
        ];

        if ($this->booking['billing_country'] === 'US') {
            $data['BillingState'] = $this->booking['billing_state'];
        }

        // Delivery details are same as billing details, as we are not shipping any goods
        $data['DeliverySurname'] = $data['BillingSurname'];
        $data['DeliveryFirstnames'] = $data['BillingFirstnames'];
        $data['DeliveryAddress1'] = $data['BillingAddress1'];
        $data['DeliveryAddress2'] = $data['BillingAddress2'];
        $data['DeliveryCity'] = $data['BillingCity'];
        $data['DeliveryPostCode'] = $data['BillingPostCode'];
        $data['DeliveryCountry'] = $data['BillingCountry'];
        $data['DeliveryPhone'] = $data['BillingPhone'];

        if (isset($data['BillingState'])) {
            $data['DeliveryState'] = $data['BillingState'];
        }

        return $opayo->paymentRequest($data);
    }

    public function getTourOptions(): array
    {
        return [
            'christmas_lights' => 'Christmas Lights Tour',
            'london_highlights_2_5_hours' => 'London Highlights Tour 2.50 x hours',
            'london_highlights_3_5_hours' => 'London Highlights Tour 3.50 x hours',
            'london_day' => 'London Day Tour',
            'harry_potter' => 'Harry Potter Tour',
            'jack_the_ripper' => 'Jack the Ripper Tour',
            'haunted_london' => 'Haunted London Tour',
            'london_by_night' => 'London by Night Tour',
            'rock_n_roll' => 'Rock n Roll Tour',
            'customized_london' => 'Customized London Tour',
            'stonehenge' => 'Stonehenge Tour',
            'windsor_castle' => 'Windsor Castle Tour',
            'customized_countryside' => 'Customized Countryside Tour'
        ];
    }

    public function getTaxiModelOptions(): array
    {
        return [
            'black_tx' => 'Black TX Model Taxi',
            'black_fairway' => 'Black Fairway Taxi (min 2 hours)',
            'silver_tx' => 'Silver TX Taxi (min 2 hours)',
            'pink_tx' => 'Pink TX Taxi (min 2 hours)',
            'mercedes_vito' => 'Mercedes Vito Taxi',
            'white_tx' => 'White TX Model Taxi',
            'white_classic_fairway' => 'White Classic Fairway (min 2 hours)',
            'black_txe_electric' => 'Black TXE Electric Taxi',
            'white_txe_electric' => 'White TXE Electric Taxi'
        ];
    }

    public function getWeddingTaxiModelOptions(): array
    {
        return [
            'black_tx_with_ribbons' => 'Black TX Model with Ribbons',
            'black_tx_without_ribbons' => 'Black TX Model without Ribbons',
            'black_fairway_with_ribbons' => 'Black Fairway with Ribbons',
            'black_fairway_without_ribbons' => 'Black Fairway without Ribbons',
            'black_mercedes_vito_6_seater_with_ribbons' => 'Black Mercedes Vito 6-seater with Ribbons',
            'black_mercedes_vito_6_seater_without_ribbons' => 'Black Mercedes Vito 6-seater without Ribbons',
            'silver_tx_with_ribbons' => 'Silver TX Model with Ribbons',
            'pink_tx_with_ribbons' => 'Pink TX Model with Ribbons',
            'white_tx_with_ribbons' => 'White TX Model with Ribbons',
            'white_classic_fairway_with_ribbons' => 'White Classic Fairway with Ribbons',
            'black_electric_txe_6_seater_with_ribbons' => 'Black Electric TXE (6-seater) with Ribbons',
            'black_electric_txe_6_seater_without_ribbons' => 'Black Electric TXE (6-seater) without Ribbons',
            'white_electric_txe_6_seater_with_ribbons' => 'White Electric TXE (6-seater) with Ribbons',
            'white_electric_txe_6_seater_without_ribbons' => 'White Electric TXE (6-seater) without Ribbons',
        ];
    }

    public function getBookingDurationOptions(): array
    {
        return $this->config['booking_duration_options'];
    }

    public function getHowFindUsOptions(): array
    {
        return [
            'yahoo' => 'Yahoo',
            'google' => 'Google',
            'lycos' => 'Lycos',
            'altavista' => 'Altavista',
            'excite' => 'Excite',
            'go_infoseek' => 'Go Infoseek',
            'recommendation' => 'Recommendation',
            'existing_customer' => 'Existing Customer',
            'travel_agent' => 'Travel Agent',
            'hotel' => 'Hotel',
            'airline' => 'Airline',
            'blog' => 'Blog',
            'business_cards' => 'Business Cards',
            'social_networking_site' => 'Social Networking Site',
            'other' => 'Other'
        ];
    }

    public function getCountryOptions(): array
    {
        return [
            "AF" => "Afghanistan",
            "AL" => "Albania",
            "DZ" => "Algeria",
            "AS" => "American Samoa",
            "AD" => "Andorra",
            "AO" => "Angola",
            "AI" => "Anguilla",
            "AQ" => "Antarctica",
            "AG" => "Antigua and Barbuda",
            "AR" => "Argentina",
            "AM" => "Armenia",
            "AW" => "Aruba",
            "AU" => "Australia",
            "AT" => "Austria",
            "AZ" => "Azerbaijan",
            "BS" => "Bahamas",
            "BH" => "Bahrain",
            "BD" => "Bangladesh",
            "BB" => "Barbados",
            "BY" => "Belarus",
            "BE" => "Belgium",
            "BZ" => "Belize",
            "BJ" => "Benin",
            "BM" => "Bermuda",
            "BT" => "Bhutan",
            "BO" => "Bolivia",
            "BA" => "Bosnia and Herzegovina",
            "BW" => "Botswana",
            "BV" => "Bouvet Island",
            "BR" => "Brazil",
            "BQ" => "British Antarctic Territory",
            "IO" => "British Indian Ocean Territory",
            "VG" => "British Virgin Islands",
            "BN" => "Brunei",
            "BG" => "Bulgaria",
            "BF" => "Burkina Faso",
            "BI" => "Burundi",
            "KH" => "Cambodia",
            "CM" => "Cameroon",
            "CA" => "Canada",
            "CT" => "Canton and Enderbury Islands",
            "CV" => "Cape Verde",
            "KY" => "Cayman Islands",
            "CF" => "Central African Republic",
            "TD" => "Chad",
            "CL" => "Chile",
            "CN" => "China",
            "CX" => "Christmas Island",
            "CC" => "Cocos [Keeling] Islands",
            "CO" => "Colombia",
            "KM" => "Comoros",
            "CG" => "Congo - Brazzaville",
            "CD" => "Congo - Kinshasa",
            "CK" => "Cook Islands",
            "CR" => "Costa Rica",
            "HR" => "Croatia",
            "CU" => "Cuba",
            "CY" => "Cyprus",
            "CZ" => "Czech Republic",
            "CI" => "Côte d'Ivoire",
            "DK" => "Denmark",
            "DJ" => "Djibouti",
            "DM" => "Dominica",
            "DO" => "Dominican Republic",
            "NQ" => "Dronning Maud Land",
            "DD" => "East Germany",
            "EC" => "Ecuador",
            "EG" => "Egypt",
            "SV" => "El Salvador",
            "GQ" => "Equatorial Guinea",
            "ER" => "Eritrea",
            "EE" => "Estonia",
            "ET" => "Ethiopia",
            "FK" => "Falkland Islands",
            "FO" => "Faroe Islands",
            "FJ" => "Fiji",
            "FI" => "Finland",
            "FR" => "France",
            "GF" => "French Guiana",
            "PF" => "French Polynesia",
            "TF" => "French Southern Territories",
            "FQ" => "French Southern and Antarctic Territories",
            "GA" => "Gabon",
            "GM" => "Gambia",
            "GE" => "Georgia",
            "DE" => "Germany",
            "GH" => "Ghana",
            "GI" => "Gibraltar",
            "GR" => "Greece",
            "GL" => "Greenland",
            "GD" => "Grenada",
            "GP" => "Guadeloupe",
            "GU" => "Guam",
            "GT" => "Guatemala",
            "GG" => "Guernsey",
            "GN" => "Guinea",
            "GW" => "Guinea-Bissau",
            "GY" => "Guyana",
            "HT" => "Haiti",
            "HM" => "Heard Island and McDonald Islands",
            "HN" => "Honduras",
            "HK" => "Hong Kong SAR China",
            "HU" => "Hungary",
            "IS" => "Iceland",
            "IN" => "India",
            "ID" => "Indonesia",
            "IR" => "Iran",
            "IQ" => "Iraq",
            "IE" => "Ireland",
            "IM" => "Isle of Man",
            "IL" => "Israel",
            "IT" => "Italy",
            "JM" => "Jamaica",
            "JP" => "Japan",
            "JE" => "Jersey",
            "JT" => "Johnston Island",
            "JO" => "Jordan",
            "KZ" => "Kazakhstan",
            "KE" => "Kenya",
            "KI" => "Kiribati",
            "KW" => "Kuwait",
            "KG" => "Kyrgyzstan",
            "LA" => "Laos",
            "LV" => "Latvia",
            "LB" => "Lebanon",
            "LS" => "Lesotho",
            "LR" => "Liberia",
            "LY" => "Libya",
            "LI" => "Liechtenstein",
            "LT" => "Lithuania",
            "LU" => "Luxembourg",
            "MO" => "Macau SAR China",
            "MK" => "Macedonia",
            "MG" => "Madagascar",
            "MW" => "Malawi",
            "MY" => "Malaysia",
            "MV" => "Maldives",
            "ML" => "Mali",
            "MT" => "Malta",
            "MH" => "Marshall Islands",
            "MQ" => "Martinique",
            "MR" => "Mauritania",
            "MU" => "Mauritius",
            "YT" => "Mayotte",
            "FX" => "Metropolitan France",
            "MX" => "Mexico",
            "FM" => "Micronesia",
            "MI" => "Midway Islands",
            "MD" => "Moldova",
            "MC" => "Monaco",
            "MN" => "Mongolia",
            "ME" => "Montenegro",
            "MS" => "Montserrat",
            "MA" => "Morocco",
            "MZ" => "Mozambique",
            "MM" => "Myanmar [Burma]",
            "NA" => "Namibia",
            "NR" => "Nauru",
            "NP" => "Nepal",
            "NL" => "Netherlands",
            "AN" => "Netherlands Antilles",
            "NT" => "Neutral Zone",
            "NC" => "New Caledonia",
            "NZ" => "New Zealand",
            "NI" => "Nicaragua",
            "NE" => "Niger",
            "NG" => "Nigeria",
            "NU" => "Niue",
            "NF" => "Norfolk Island",
            "KP" => "North Korea",
            "VD" => "North Vietnam",
            "MP" => "Northern Mariana Islands",
            "NO" => "Norway",
            "OM" => "Oman",
            "PC" => "Pacific Islands Trust Territory",
            "PK" => "Pakistan",
            "PW" => "Palau",
            "PS" => "Palestinian Territories",
            "PA" => "Panama",
            "PZ" => "Panama Canal Zone",
            "PG" => "Papua New Guinea",
            "PY" => "Paraguay",
            "YD" => "People's Democratic Republic of Yemen",
            "PE" => "Peru",
            "PH" => "Philippines",
            "PN" => "Pitcairn Islands",
            "PL" => "Poland",
            "PT" => "Portugal",
            "PR" => "Puerto Rico",
            "QA" => "Qatar",
            "RO" => "Romania",
            "RU" => "Russia",
            "RW" => "Rwanda",
            "RE" => "Réunion",
            "BL" => "Saint Barthélemy",
            "SH" => "Saint Helena",
            "KN" => "Saint Kitts and Nevis",
            "LC" => "Saint Lucia",
            "MF" => "Saint Martin",
            "PM" => "Saint Pierre and Miquelon",
            "VC" => "Saint Vincent and the Grenadines",
            "WS" => "Samoa",
            "SM" => "San Marino",
            "SA" => "Saudi Arabia",
            "SN" => "Senegal",
            "RS" => "Serbia",
            "CS" => "Serbia and Montenegro",
            "SC" => "Seychelles",
            "SL" => "Sierra Leone",
            "SG" => "Singapore",
            "SK" => "Slovakia",
            "SI" => "Slovenia",
            "SB" => "Solomon Islands",
            "SO" => "Somalia",
            "ZA" => "South Africa",
            "GS" => "South Georgia and the South Sandwich Islands",
            "KR" => "South Korea",
            "ES" => "Spain",
            "LK" => "Sri Lanka",
            "SD" => "Sudan",
            "SR" => "Suriname",
            "SJ" => "Svalbard and Jan Mayen",
            "SZ" => "Swaziland",
            "SE" => "Sweden",
            "CH" => "Switzerland",
            "SY" => "Syria",
            "ST" => "São Tomé and Príncipe",
            "TW" => "Taiwan",
            "TJ" => "Tajikistan",
            "TZ" => "Tanzania",
            "TH" => "Thailand",
            "TL" => "Timor-Leste",
            "TG" => "Togo",
            "TK" => "Tokelau",
            "TO" => "Tonga",
            "TT" => "Trinidad and Tobago",
            "TN" => "Tunisia",
            "TR" => "Turkey",
            "TM" => "Turkmenistan",
            "TC" => "Turks and Caicos Islands",
            "TV" => "Tuvalu",
            "UM" => "U.S. Minor Outlying Islands",
            "PU" => "U.S. Miscellaneous Pacific Islands",
            "VI" => "U.S. Virgin Islands",
            "UG" => "Uganda",
            "UA" => "Ukraine",
            "SU" => "Union of Soviet Socialist Republics",
            "AE" => "United Arab Emirates",
            "GB" => "United Kingdom",
            "US" => "United States",
            "UY" => "Uruguay",
            "UZ" => "Uzbekistan",
            "VU" => "Vanuatu",
            "VA" => "Vatican City",
            "VE" => "Venezuela",
            "VN" => "Vietnam",
            "WK" => "Wake Island",
            "WF" => "Wallis and Futuna",
            "EH" => "Western Sahara",
            "YE" => "Yemen",
            "ZM" => "Zambia",
            "ZW" => "Zimbabwe",
            "AX" => "Åland Islands",
        ];
    }

    public function getStateOptions(): array
    {
        return [
            'AL' => 'Alabama',
            'AK' => 'Alaska',
            'AZ' => 'Arizona',
            'AR' => 'Arkansas',
            'CA' => 'California',
            'CO' => 'Colorado',
            'CT' => 'Connecticut',
            'DE' => 'Delaware',
            'DC' => 'District Of Columbia',
            'FL' => 'Florida',
            'GA' => 'Georgia',
            'HI' => 'Hawaii',
            'ID' => 'Idaho',
            'IL' => 'Illinois',
            'IN' => 'Indiana',
            'IA' => 'Iowa',
            'KS' => 'Kansas',
            'KY' => 'Kentucky',
            'LA' => 'Louisiana',
            'ME' => 'Maine',
            'MD' => 'Maryland',
            'MA' => 'Massachusetts',
            'MI' => 'Michigan',
            'MN' => 'Minnesota',
            'MS' => 'Mississippi',
            'MO' => 'Missouri',
            'MT' => 'Montana',
            'NE' => 'Nebraska',
            'NV' => 'Nevada',
            'NH' => 'New Hampshire',
            'NJ' => 'New Jersey',
            'NM' => 'New Mexico',
            'NY' => 'New York',
            'NC' => 'North Carolina',
            'ND' => 'North Dakota',
            'OH' => 'Ohio',
            'OK' => 'Oklahoma',
            'OR' => 'Oregon',
            'PA' => 'Pennsylvania',
            'RI' => 'Rhode Island',
            'SC' => 'South Carolina',
            'SD' => 'South Dakota',
            'TN' => 'Tennessee',
            'TX' => 'Texas',
            'UT' => 'Utah',
            'VT' => 'Vermont',
            'VA' => 'Virginia',
            'WA' => 'Washington',
            'WV' => 'West Virginia',
            'WI' => 'Wisconsin',
            'WY' => 'Wyoming'
        ];
    }

    public function getCaptchaQuestion(): array
    {
        return [random_int(1, 10), random_int(1, 10)];
    }

    public function logSubmission(string $ipAddress): void
    {
        $now = new DateTimeImmutable();

        // Always insert a new row as it doesn't matter if we have multiple
        // entries for the same IP address
        $sql = 'INSERT INTO booking_form_submissions_log (ip_address, submitted) VALUES (:ip_address, :submitted)';
        $sth = $this->connection->prepare($sql);
        $sth->bindValue('ip_address', $ipAddress, PDO::PARAM_STR);
        $sth->bindValue('submitted', $now->format(self::MYSQL_DATETIME_FORMAT), PDO::PARAM_STR);
        $sth->execute();
    }

    public function countPreviousSubmissions(string $ipAddress): int
    {
        $now = new DateTimeImmutable();
        $cutoff = $now->modify('-' . self::FORM_SUBMISSION_PERIOD);

        $this->deleteSubmissions($cutoff);

        $sql = 'SELECT ip_address FROM booking_form_submissions_log WHERE ip_address = :ip_address';
        $sth = $this->connection->prepare($sql);
        $sth->bindValue('ip_address', $ipAddress, PDO::PARAM_STR);
        $sth->execute();

        return $sth->rowCount();
    }

    public function deleteSubmissions(DateTimeInterface $cutoff): void
    {
        $sql = 'DELETE FROM booking_form_submissions_log WHERE submitted < :cutoff';
        $sth = $this->connection->prepare($sql);
        $sth->bindValue('cutoff', $cutoff->format(self::MYSQL_DATETIME_FORMAT), PDO::PARAM_STR);
        $sth->execute();
    }

    public function getCommonAddresses(): array
    {
        $addresses = [];

        $addresses[] = [
            'name' => 'Airport: Heathrow (LHR) T2',
            'address1' => 'Terminal 2',
            'address2' => 'Hounslow',
            'city' => 'London',
            'postcode' => 'TW6 1AH'
        ];

        $addresses[] = [
            'name' => 'Airport: Heathrow (LHR) T3',
            'address1' => 'Terminal 3',
            'address2' => 'Hounslow',
            'city' => 'London',
            'postcode' => 'TW6 1QG'
        ];

        $addresses[] = [
            'name' => 'Airport: Heathrow (LHR) T4',
            'address1' => 'Terminal 4',
            'address2' => 'Hounslow',
            'city' => 'London',
            'postcode' => 'TW6 2GW'
        ];

        $addresses[] = [
            'name' => 'Airport: Heathrow (LHR) T5',
            'address1' => 'Terminal 5',
            'address2' => 'Hounslow',
            'city' => 'London',
            'postcode' => 'TW6 2GA'
        ];

        $addresses[] = [
            'name' => 'Airport: London City (LCY)',
            'address1' => 'Hartmann Road',
            'address2' => 'London',
            'city' => 'London',
            'postcode' => 'E16 2PX'
        ];

        $addresses[] = [
            'name' => 'Airport: Gatwick (LGW) North',
            'address1' => 'Terminal N (North)',
            'address2' => 'Horley, Gatwick',
            'city' => 'London',
            'postcode' => 'RH6 0PJ'
        ];

        $addresses[] = [
            'name' => 'Airport: Gatwick (LGW) South',
            'address1' => 'Terminal S (South)',
            'address2' => 'Horley, Gatwick',
            'city' => 'London',
            'postcode' => 'RH6 0NP'
        ];

        $addresses[] = [
            'name' => 'Airport: Luton (LTN)',
            'address1' => 'Airport Way',
            'address2' => 'Luton',
            'city' => 'London',
            'postcode' => 'LU2 9QT'
        ];

        $addresses[] = [
            'name' => 'Airport: Stansted (STN)',
            'address1' => 'Bassingbourn Road',
            'address2' => 'Stansted',
            'city' => 'London',
            'postcode' => 'CM24 1QW'
        ];

        $addresses[] = [
            'name' => 'Airport: Southend (SEN)',
            'address1' => 'Eastwoodbury Crescent',
            'address2' => 'Southend-on-Sea',
            'city' => 'London',
            'postcode' => 'SS2 6YF'
        ];

        $addresses[] = [
            'name' => 'Rail Station: St. Pancras',
            'address1' => 'Euston Road',
            'address2' => 'Euston',
            'city' => 'London',
            'postcode' => 'N1C 4QP'
        ];

        $addresses[] = [
            'name' => 'Rail Station: Kings Cross',
            'address1' => 'Euston Road',
            'address2' => 'Kings Cross',
            'city' => 'London',
            'postcode' => 'N1 9AL'
        ];

        $addresses[] = [
            'name' => 'Rail Station: London Bridge',
            'address1' => 'Station Approach Road',
            'address2' => 'Southwark',
            'city' => 'London',
            'postcode' => 'SE1 2SW'
        ];

        $addresses[] = [
            'name' => 'Rail Station: Paddington',
            'address1' => 'Praed Street',
            'address2' => 'Paddington',
            'city' => 'London',
            'postcode' => 'W2 1HQ'
        ];

        $addresses[] = [
            'name' => 'Rail Station: Marylebone',
            'address1' => 'Melcombe Place',
            'address2' => 'Marylebone',
            'city' => 'London',
            'postcode' => 'NW1 6JJ'
        ];

        $addresses[] = [
            'name' => 'Rail Station: Euston',
            'address1' => 'Euston Road',
            'address2' => 'Euston',
            'city' => 'London',
            'postcode' => 'NW1 2RT'
        ];

        $addresses[] = [
            'name' => 'Rail Station: Victoria',
            'address1' => 'Victoria Street',
            'address2' => 'Victoria',
            'city' => 'London',
            'postcode' => 'SW1E 5ND'
        ];

        $addresses[] = [
            'name' => 'Rail Station: Charing Cross',
            'address1' => 'Strand',
            'address2' => 'Westminster',
            'city' => 'London',
            'postcode' => 'WC2N 5HF'
        ];

        $addresses[] = [
            'name' => 'Rail Station: Waterloo',
            'address1' => 'Waterloo Road',
            'address2' => 'Waterloo',
            'city' => 'London',
            'postcode' => 'SE1 8SW'
        ];

        $addresses[] = [
            'name' => 'Rail Station: Liverpool Street',
            'address1' => 'Liverpool Street',
            'address2' => 'Bishopsgate',
            'city' => 'London',
            'postcode' => 'EC2M 7PY'
        ];

        $addresses[] = [
            'name' => 'Rail Station: Fenchurch Street',
            'address1' => 'Fenchurch Place',
            'address2' => 'Fenchurch',
            'city' => 'London',
            'postcode' => 'EC3M 4AJ'
        ];

        $addresses[] = [
            'name' => 'Port: Greenwich Pier',
            'address1' => 'Cutty Sark Gardens, King William Walk',
            'address2' => 'Greenwich',
            'city' => 'London',
            'postcode' => 'SE10 9HT'
        ];

        $addresses[] = [
            'name' => 'Port: London International Cruise Terminal (LICT), Tilbury',
            'address1' => 'Tilbury Docks',
            'address2' => 'Ferry Road',
            'city' => 'Tilbury, Essex',
            'postcode' => 'RM18 7NG'
        ];

        $addresses[] = [
            'name' => 'Port: Southampton, City Terminal',
            'address1' => 'Berth 101, Western Docks',
            'address2' => 'Herbert Walker Avenue',
            'city' => 'Southampton',
            'postcode' => 'SO15 1HJ'
        ];

        $addresses[] = [
            'name' => 'Port: Southampton, Ocean Terminal',
            'address1' => 'Berth 46-49, Dock Gate 4',
            'address2' => 'Cunard Road',
            'city' => 'Southampton',
            'postcode' => 'SO14 3QN'
        ];

        $addresses[] = [
            'name' => 'Port: Southampton, Mayflower Terminal',
            'address1' => 'Berth 106, Dock Gate 10',
            'address2' => 'Herbert Walker Avenue',
            'city' => 'Southampton',
            'postcode' => 'SO15 1HJ'
        ];

        $addresses[] = [
            'name' => 'Port: Southampton, Queen Elizabeth Terminal',
            'address1' => 'Berth 38/39, Dock Gate 4',
            'address2' => 'Test Road',
            'city' => 'Southampton',
            'postcode' => 'SO14 3GG'
        ];

        $addresses[] = [
            'name' => 'Port: Dover, Terminal 1',
            'address1' => 'Cruise Terminal 1',
            'address2' => 'Lord Warden Square',
            'city' => 'Dover',
            'postcode' => 'CT17 9EQ'
        ];

        $addresses[] = [
            'name' => 'Port: Dover, Terminal 2',
            'address1' => 'Cruise Terminal 2',
            'address2' => 'Lord Warden Square',
            'city' => 'Dover',
            'postcode' => 'CT17 9EQ'
        ];

        return $addresses;
    }
}
