<?php

namespace Mtc\Core;

/**
 * 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
        );
    }

    /**
     * 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);
    }
}
