<?php

namespace Mtc\Core;

use Illuminate\Support\Facades\DB;

/**
 * UK Postcode class.
 * What it can do:
 * - Normalize postcodes - bring 'dd13ja' to 'DD1 3JA'
 * - Parse potcodes into parts - inward code, outward, area etc.
 * - Validate the postcode agains the RegEx
 *
 * Example postcode used in below examples: DD1 3JA
 *
 * Description of the UK Postcode format
 * https://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom#Formatting
 *
 * @author  Aleksey Lavrinenko
 * @version 25.02.2016.
 */
class UkPostcode implements Postcode
{
    /**
     * Normalizes postcode format to uppercase with space.
     * Brings dd13ja to DD1 3JA
     *
     * @param $postcode_string
     *
     * @return string
     */
    // 1) Make normalize() robust (strip junk, enforce single space)
    public function normalize($postcode_string)
    {
        $s = strtoupper((string) $postcode_string);
        // keep only letters & digits
        $s = preg_replace('/[^A-Z0-9]/i', '', $s) ?? '';
        $len = strlen($s);

        // If we have at least 4 chars, put a single space before the final 3
        if ($len > 3) {
            return substr($s, 0, $len - 3).' '.substr($s, -3);
        }

        // Otherwise return as is (constructor guards against missing parts)
        return $s;
    }

    /**
     * Checks if a string validates as a UK postcode. The Regex was taken from the Royal Mail website with two
     * adjustments:
     * - space in the middle of postcode has been made unmandatory
     * - added parentheses around the outward core part of regex, otherwise the 'start of string' char didn't work
     * (due to it being treated as optional by the or | bit)
     *
     * @param string $postcode
     *
     * @return int
     */
    // 3) Simpler, safe validates()
    public function validates($postcode = '')
    {
        if ($postcode === '') {
            $postcode = (string) $this;
        }

        return (int) preg_match(
            '/^(([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([A-Za-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z]))))) ?[0-9][A-Za-z]{2})$/',
            $postcode,
        );
    }

    /**
     * Connect to PAF and retrieve address returned as a concatenated string.
     * Known default usages in member registration & checkout basket if PAF_ENABLED.
     *
     * @param string $postcode Postcode to find
     *
     * @return array list of addresses found
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public static function retrievePAFAddresses(string $postcode): array
    {
        return DB::connection('paf')
            ->table('Address_Complete')
            ->select([
                'Organisation Name',
                'Building Number',
                'Building Name',
                'Sub Building Name',
                'Thoroughfare Name',
                'Thoroughfare Descriptor',
                'Dependent Locality',
                'Post Town',
                'Department Name',
                'PO Box Number',
                'Dependent Thoroughfare Name',
                'Dependent Thoroughfare Descriptor',
            ])
            ->where('Postcode', $postcode)
            ->orderBy('PO Box Number', 'ASC')
            ->orderBy('Department Name', 'ASC')
            ->orderBy('Organisation Name', 'ASC')
            ->orderBy('Building Number', 'ASC')
            ->orderBy('Building Name', 'ASC')
            ->orderBy('Sub Building Name', 'ASC')
            ->orderBy('Address Key', 'ASC')
            ->get()
            ->map(static function ($pafAddress) {
                $data = (array) $pafAddress;

                $organisation_name = $data['Organisation Name'];
                $department_name = $data['Department Name'];
                $po_box = $data['PO Box Number'];
                $building_number = $data['Building Number'];
                $building_name = $data['Building Name'];
                $sub_building_name = $data['Sub Building Name'];
                $thoroughfare_name = $data['Thoroughfare Name'];
                $thoroughfare_descriptor = $data['Thoroughfare Descriptor'];
                $dependent_locality = $data['Dependent Locality'];
                $dependent_thoroughfare_name = $data['Dependent Thoroughfare Name'];
                $dependent_thoroughfare_descriptor = $data['Dependent Thoroughfare Descriptor'];
                $post_town = $data['Post Town'];

                $address = [];
                $address['display_name'] = '';

                if (! empty($department_name)) {
                    $address['display_name'] .= $department_name.' ';
                    $address['department_name'] = $department_name;
                }
                if (! empty($organisation_name)) {
                    $address['display_name'] .= $organisation_name.' ';

                    $address['organisation_name'] = $organisation_name;
                }
                if (! empty($department_name) || ! empty($po_box)) {
                    $address['display_name'] .= 'POBox '.$po_box.' ';

                    $address['po_box'] = $po_box;
                }
                if (! empty($organisation_name) || ! empty($po_box)) {
                    $address['display_name'] = rtrim($address['display_name']).'| ';
                }
                if (! empty($sub_building_name)) {
                    $address['display_name'] .= $sub_building_name.' ';

                    $address['sub_building_name'] = $sub_building_name;
                }
                if (! empty($building_name)) {
                    $address['display_name'] .= $building_name.' ';

                    $address['building_name'] = $building_name;
                }

                /*
                 * If at least one of the params for first section (address1) of the address is set
                 * replace the ending whitespace with comma and whitespace
                 */
                if (
                    ! empty($department_name)
                    || ! empty($organisation_name)
                    || ! empty($po_box)
                    || ! empty($sub_building_name)
                    || ! empty($building_name)
                ) {
                    $address['display_name'] = rtrim($address['display_name']).', ';
                }

                if ($building_number != '' && $building_number != 0) {
                    $address['display_name'] .= $building_number.' ';

                    $address['building_number'] = $building_number;
                }
                if (! empty($thoroughfare_name)) {
                    $address['display_name'] .= $thoroughfare_name.' ';

                    $address['thoroughfare_name'] = $thoroughfare_name;
                }
                if (! empty($thoroughfare_descriptor)) {
                    $address['display_name'] .= $thoroughfare_descriptor.'| ';

                    $address['thoroughfare_descriptor'] = $thoroughfare_descriptor;
                }
                if (! empty($dependent_thoroughfare_name)) {
                    $address['display_name'] .= $dependent_thoroughfare_name.' ';

                    $address['dependent_thoroughfare_name'] = $dependent_thoroughfare_name;
                }
                if (! empty($dependent_thoroughfare_descriptor)) {
                    $address['display_name'] .= $dependent_thoroughfare_descriptor.' ';

                    $address['dependent_thoroughfare_descriptor'] = $dependent_thoroughfare_descriptor;
                }
                if (
                    ! empty($thoroughfare_descriptor)
                    && empty($dependent_thoroughfare_name)
                    && empty($dependent_thoroughfare_descriptor)
                ) {
                    $address['display_name'] = rtrim($address['display_name']);
                }

                /*
                 * If at least one of the params for second section (address2) of the address is set
                 * replace the ending whitespace with comma and whitespace
                 */
                if (
                    ! empty($thoroughfare_name)
                    || ! empty($thoroughfare_descriptor)
                    || ! empty($dependent_thoroughfare_name)
                    || ! empty($dependent_thoroughfare_descriptor)
                ) {
                    $address['display_name'] = substr($address['display_name'], 0, -1).', ';
                }

                if (! empty($dependent_locality)) {
                    $address['display_name'] .= $dependent_locality.' ';

                    $address['dependent_locality'] = $dependent_locality;
                }
                if (! empty($dependent_locality)) {
                    $address['display_name'] = rtrim($address['display_name']).', ';
                }

                if (! empty($post_town)) {
                    $address['display_name'] .= $post_town;

                    $address['post_town'] = $post_town;
                }

                foreach ($address as $key => $name) {
                    $address[$key] = ucwords(strtolower($name));
                }

                return $address;
            })
            ->toArray();
    }

    /**
     * The 'DD1' part of 'DD1 3JA' postcode
     *
     * @var string
     */
    protected $outward_code;

    /**
     * The '3JA' part of 'DD1 3JA' postcode
     *
     * @var string
     */
    protected $inward_code;

    /**
     * The 'DD' part of 'DD1 3JA' postcode
     *
     * @var string
     */
    protected $area;

    /**
     * The postcode district is the outward code. In our case this is just the number - '1' of 'DD1'
     * of 'DD1 3JA' postcode
     *
     * @var string
     */
    protected $district;

    /**
     * The '3' part
     *
     * @var string
     */
    protected $sector;

    /**
     * The 'JA' part of 'DD1 3JA' postcode
     *
     * @var string
     */
    protected $unit;

    /**
     * @return string
     */
    public function getOutwardCode()
    {
        return $this->outward_code;
    }

    /**
     * @return string
     */
    public function getInwardCode()
    {
        return $this->inward_code;
    }

    /**
     * @return string
     */
    public function getArea()
    {
        return $this->area;
    }

    /**
     * @param bool $full If true, will return the full district including the area code.
     *
     * @return string
     */
    public function getDistrict($full = false)
    {
        return $full ? $this->area.$this->district : $this->district;
    }

    /**
     * @param bool $full If true, will return the full district including the area code and district.
     *
     * @return string
     */
    public function getSector($full = false)
    {
        return $full ? $this->getDistrict(true).' '.$this->sector : $this->sector;
    }

    /**
     * @return string
     */
    public function getUnit()
    {
        return $this->unit;
    }

    /**
     * UkPostcode constructor.
     *
     * @param string $postcode_string Postcode as string
     */
    // 2) Guard the constructor against invalid formats
    public function __construct($postcode_string)
    {
        $postcode_string = self::normalize($postcode_string);

        // Safely split into outward/inward
        $parts = explode(' ', $postcode_string, 2);
        $this->outward_code = $parts[0] ?? '';
        $this->inward_code = $parts[1] ?? '';

        // Try to parse; if it doesn't match, leave parts empty (no warnings)
        if (preg_match(
            '/^(([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([A-Za-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z]))))) ?[0-9][A-Za-z]{2})$/',
            $postcode_string,
            $m,
        )) {
            // Matches: break into area/district/sector/unit
            preg_match('#^([A-Z]+)(\d+[A-Z]?) (\d)([A-Z]+)$#', $postcode_string, $m2);
            $this->area = $m2[1] ?? '';
            $this->district = $m2[2] ?? '';
            $this->sector = $m2[3] ?? '';
            $this->unit = $m2[4] ?? '';
        } else {
            // Invalid format → keep parts blank to avoid notices; validates() will fail
            $this->area = $this->district = $this->sector = $this->unit = '';
        }
    }

    /**
     * @return string
     */
    // 4) Robust string cast (no trailing space if inward is missing)
    public function __toString()
    {
        return trim($this->outward_code.' '.$this->inward_code);
    }

    /**
     * Finds postcode coordinates by postcode
     *
     * @param $postcode
     * @return array|object
     * @throws \Exception
     */
    public static function retrievePostcodeCoordinates($postcode)
    {
        $location = DB::connection('paf')
            ->table('postcode_geo')
            ->where('postcode', $postcode)
            ->first();
        if (empty($location)) {
            return [];
        }

        return (array) $location;
    }
}
