<?php

class Tracking_USPS {

    /**
     * Sometimes the API returns a city but no state or ZIP. This is a lookup table of city => state
     * so the API can return a more complete response.
     */
    public const STATELESS_CITIES = [
        "LOS ANGELES" => "CA",
        "SAN FRANCISCO" => "CA",
        "NEW YORK" => "NY",
        "CHICAGO" => "IL",
        "MIAMI" => "FL"
    ];

    /**
     *
     * @param string $code
     * @return \TrackingInfo
     * @throws TrackingException
     */
    public static function track(string $code, string $carrier = ""): TrackingInfo {
        $barcode = new TrackingBarcode($code);

        try {
            $track = new \SimpleXMLElement("<TrackFieldRequest></TrackFieldRequest>");
            $track->addAttribute('USERID', env("usps_user_id"));
            $track->addChild('Revision', '1');
            $track->addChild('ClientIp', $_SERVER['REMOTE_ADDR']);
            $track->addChild('SourceId', env("usps_source_id", "selfhosted-opensource-default-value.netsyms.net"));
            $pack = $track->addChild('TrackID');
            $pack->addAttribute('ID', $barcode->getSanitized());
            $url = 'https://secure.shippingapis.com/ShippingApi.dll?API=TrackV2&XML=' . $track->asXML();
            $xml = simplexml_load_file($url);

            if ($xml->getName() == "Error") {
                if (!empty($xml->Description)) {
                    throw new TrackingException("The USPS tracking system is having problems: \"" . trim($xml->Description) . "\"");
                }
                throw new TrackingException("The USPS tracking system is having problems. Try again later.");
            }

            if (!empty($xml->TrackInfo)) {
                $trackinfo = $xml->TrackInfo;
            }
            if (!empty($xml->TrackInfo->Error)) {
                throw new TrackingException(str_replace("<SUP>&reg;</SUP>", "®", (string) $xml->TrackInfo->Error->Description));
            }
        } catch (TrackingException $ex) {
            throw $ex;
        } catch (Exception $ex) {
            throw new TrackingException("There was a server problem. This code cannot be tracked right now.");
        }

        $info = new TrackingInfo();

        try {
            $info->setCode($trackinfo->attributes()["ID"]);
        } catch (Exception $ex) {
            throw new TrackingException("The USPS tracking system returned an invalid response. Try again later.");
        }

        $info->setCarrier("usps");
        $info->setService(new Service((string) $trackinfo->ClassofMailCode, (string) $trackinfo->Class));
        $info->setCarrierAttributionText(CarrierAssets::getAttribution(Carriers::getCarrierCode($info->getCarrier())));
        $info->setCarrierLogo(CarrierAssets::getLogo(Carriers::getCarrierCode($info->getCarrier())));

        $current_status = new TrackingEntry(
                TrackingStatus::USPSEventCodeToStatus($trackinfo->TrackSummary->EventCode),
                ($trackinfo->StatusSummary ?? "Unknown") . (TrackingStatus::USPSEventCodeToStatus($trackinfo->TrackSummary->EventCode) == TrackingStatus::TRACKING_STATUS_UNKNOWN ? " " . $trackinfo->TrackSummary->EventCode : ""),
                $trackinfo->TrackSummary->EventDate . " " . $trackinfo->TrackSummary->EventTime,
                null,
                TrackingStatus::isUSPSEventCodeContainerScan($trackinfo->TrackSummary->EventCode)
        );

        $current_location = new Location();
        $current_location->city = (string) $trackinfo->TrackSummary->EventCity ?? "";
        $current_location->state = (string) $trackinfo->TrackSummary->EventState ?? "";
        $current_location->zip = (string) $trackinfo->TrackSummary->EventZIPCode ?? "";
        $current_location->country = (string) $trackinfo->TrackSummary->EventCountry ?? "";

        /*
         * Fill in state from list above when it's missing from the API response
         */
        if ($current_location->state == "" && $current_location->zip == "") {
            if (array_key_exists(strtoupper($current_location->city), self::STATELESS_CITIES)) {
                $current_location->state = self::STATELESS_CITIES[$current_location->city];
            }
        }

        $current_status->setLocation($current_location);

        $info->setCurrentStatus($current_status);
        // USPS doesn't put the latest entry in the history
        $info->appendHistoryEntry($current_status);

        $from = new Location();
        $from->city = (string) $trackinfo->OriginCity ?? "";
        $from->state = (string) $trackinfo->OriginState ?? "";
        $from->zip = (string) $trackinfo->OriginZip ?? "";
        $from->country = (string) $trackinfo->OriginCountryCode ?? "";

        $info->setFrom($from);

        $to = new Location();
        $to->city = (string) $trackinfo->DestinationCity ?? "";
        $to->state = (string) $trackinfo->DestinationState ?? "";
        $to->zip = (string) $trackinfo->DestinationZip ?? "";
        $to->country = (string) $trackinfo->DestinationCountryCode ?? "";

        $info->setTo($to);

        for ($i = 0; $i < count($trackinfo->TrackDetail); $i++) {
            $history = $trackinfo->TrackDetail[$i];
            $location = new Location();
            $location->city = (string) $history->EventCity ?? "";
            $location->state = (string) $history->EventState ?? "";
            $location->zip = (string) $history->EventZIPCode ?? "";
            $location->country = (string) $history->EventCountry ?? "";
            /*
             * Fill in state from list above when it's missing from the API response
             */
            if ($location->state == "" && $location->zip == "") {
                if (array_key_exists(strtoupper($location->city), self::STATELESS_CITIES)) {
                    $location->state = self::STATELESS_CITIES[$location->city];
                }
            }
            if ((empty($history->EventDate) || empty($history->EventTime)) && $i < count($trackinfo->TrackDetail) - 1) {
                // If there's no date/time for some reason (yes this happens sometimes apparently),
                // just make it 60 seconds after the previous event.
                // This way it'll be in the correct order if the events are displayed
                // after being sorted by date/time.
                // Because events are ordered latest first, we get $i+1 to get the previous event.
                $datetime = date("Y-m-d H:i:s", strtotime($trackinfo->TrackDetail[$i + 1]->EventDate . " " . $trackinfo->TrackDetail[$i + 1]->EventTime) + 60);
            } else {
                $datetime = $history->EventDate . " " . $history->EventTime;
            }
            $info->appendHistoryEntry(new TrackingEntry(
                            TrackingStatus::USPSEventCodeToStatus((string) $history->EventCode),
                            ((string) $history->Event) . (TrackingStatus::USPSEventCodeToStatus((string) $history->EventCode) == TrackingStatus::TRACKING_STATUS_UNKNOWN ? " " . (string) $history->EventCode : ""),
                            $datetime,
                            $location,
                            TrackingStatus::isUSPSEventCodeContainerScan((string) $history->EventCode)));
        }

        return $info;
    }

}
