<?php
/**
 * Tax / VAT calculation.
 *
 * Added in 4.4.3. Before this the plugin had no concept of tax at all, which
 * is a compliance blocker rather than a missing convenience: an EU business
 * must show the VAT rate and amount on what it issues to a customer, and most
 * of the world requires the tax component to be stated somewhere on the
 * receipt.
 *
 * DESIGN NOTES
 * ------------
 * - Tax is computed ONCE, at booking time, and the resulting breakdown is
 *   stored on the booking row. It is never recomputed for display. If the shop
 *   changes its VAT rate next year, historical bookings and their invoices
 *   must keep showing the rate that was actually charged.
 * - Money is handled in decimal and rounded once, at the end. Rounding each
 *   line and then summing produces totals that disagree with the invoice by a
 *   cent, which accountants do notice.
 * - Two pricing modes, because both are normal depending on market:
 *     exclusive -- prices are net, tax is added on top (typical US)
 *     inclusive -- prices already contain tax, we back it out (typical EU/AU)
 *
 * @package Yatoon_Booking_System
 * @since 4.4.3
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class YATOON_Tax {

    /**
     * Is tax handling switched on?
     *
     * @return bool
     */
    public static function is_enabled() {
        return get_option( 'yatoon_tax_enabled', '0' ) === '1';
    }

    /**
     * Are configured prices tax-inclusive?
     *
     * @return bool
     */
    public static function prices_include_tax() {
        return get_option( 'yatoon_tax_prices_include', '0' ) === '1';
    }

    /**
     * The label shown to customers, e.g. "VAT", "GST", "Sales Tax".
     *
     * @return string
     */
    public static function label() {
        $label = trim( (string) get_option( 'yatoon_tax_label', '' ) );
        return '' !== $label ? $label : __( 'Tax', 'yatoon-booking-system' );
    }

    /**
     * The default tax rate as a percentage.
     *
     * @return float
     */
    public static function default_rate() {
        $rate = (float) get_option( 'yatoon_tax_rate', 0 );
        return max( 0.0, min( 100.0, $rate ) );
    }

    /**
     * The rate that applies to a given service.
     *
     * A per-service override exists because real menus mix rates: in several
     * EU countries a haircut and a retail product sold alongside it are taxed
     * differently, and some services are zero-rated or exempt.
     *
     * @param int|null $service_id
     * @return float Percentage.
     */
    public static function rate_for_service( $service_id = null ) {
        $rate = self::default_rate();

        $service_id = (int) $service_id;
        if ( $service_id > 0 ) {
            global $wpdb;
            // Column is added by the 4.4.3 migration; guard for the window
            // between plugin update and migration run.
            $override = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                "SELECT tax_rate FROM {$wpdb->prefix}yatoon_services WHERE id = %d",
                $service_id
            ) );
            // NULL means "use the shop default"; 0 means "deliberately zero-rated".
            if ( null !== $override && '' !== $override ) {
                $rate = max( 0.0, min( 100.0, (float) $override ) );
            }
        }

        /**
         * Filter the tax rate applied to a service.
         *
         * @since 4.4.3
         * @param float    $rate       Percentage.
         * @param int|null $service_id
         */
        return (float) apply_filters( 'yatoon_tax_rate', $rate, $service_id );
    }

    /**
     * Split a gross or net amount into its net / tax / gross parts.
     *
     * @param float    $amount     Price as configured (net or gross per settings).
     * @param int|null $service_id
     * @return array{net: float, tax: float, gross: float, rate: float, inclusive: bool}
     */
    public static function breakdown( $amount, $service_id = null ) {
        $amount = (float) $amount;
        $rate   = self::is_enabled() ? self::rate_for_service( $service_id ) : 0.0;

        if ( $rate <= 0 ) {
            return array(
                'net'       => round( $amount, 2 ),
                'tax'       => 0.0,
                'gross'     => round( $amount, 2 ),
                'rate'      => 0.0,
                'inclusive' => self::prices_include_tax(),
            );
        }

        if ( self::prices_include_tax() ) {
            // Price already contains tax: net = gross / (1 + r)
            $gross = $amount;
            $net   = $gross / ( 1 + ( $rate / 100 ) );
            $tax   = $gross - $net;
        } else {
            $net   = $amount;
            $tax   = $net * ( $rate / 100 );
            $gross = $net + $tax;
        }

        return array(
            'net'       => round( $net, 2 ),
            'tax'       => round( $tax, 2 ),
            'gross'     => round( $gross, 2 ),
            'rate'      => $rate,
            'inclusive' => self::prices_include_tax(),
        );
    }

    /**
     * The amount actually charged for a configured price.
     *
     * Exclusive pricing adds tax on top; inclusive pricing does not change the
     * number the customer was shown.
     *
     * @param float    $amount
     * @param int|null $service_id
     * @return float
     */
    public static function payable_total( $amount, $service_id = null ) {
        $breakdown = self::breakdown( $amount, $service_id );
        return $breakdown['gross'];
    }

    /**
     * Tax fields to store on a booking row.
     *
     * Returns an empty array when the tax columns are not present yet, so a
     * booking placed between plugin update and migration still saves.
     *
     * @param float    $amount     The booking total as configured.
     * @param int|null $service_id
     * @return array<string, mixed>
     */
    public static function booking_fields( $amount, $service_id = null ) {
        if ( ! self::columns_exist() ) {
            return array();
        }

        $breakdown = self::breakdown( $amount, $service_id );

        return array(
            'tax_rate'      => $breakdown['rate'],
            'tax_amount'    => $breakdown['tax'],
            'net_amount'    => $breakdown['net'],
            'tax_inclusive' => $breakdown['inclusive'] ? 1 : 0,
        );
    }

    /**
     * Are the 4.4.3 tax columns available?
     *
     * @return bool
     */
    public static function columns_exist() {
        static $exists = null;
        if ( null !== $exists ) {
            return $exists;
        }

        global $wpdb;
        $exists = (bool) $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            "SHOW COLUMNS FROM {$wpdb->prefix}yatoon_bookings LIKE %s",
            'tax_amount'
        ) );

        return $exists;
    }

    /**
     * Format a money amount using the shop currency.
     *
     * @param float $amount
     * @return string
     */
    public static function format_money( $amount ) {
        $currency = strtoupper( (string) get_option( 'yatoon_currency', 'USD' ) );
        $symbols  = array(
            'USD' => '$',  'EUR' => "\xE2\x82\xAC", 'GBP' => "\xC2\xA3",
            'JPY' => "\xC2\xA5", 'CNY' => "\xC2\xA5", 'AUD' => 'A$',
            'CAD' => 'C$', 'NZD' => 'NZ$', 'HKD' => 'HK$', 'SGD' => 'S$',
            'KRW' => "\xE2\x82\xA9", 'INR' => "\xE2\x82\xB9",
        );

        $zero_decimal = in_array( $currency, array( 'JPY', 'KRW', 'VND', 'CLP' ), true );
        $value        = number_format( (float) $amount, $zero_decimal ? 0 : 2 );

        return isset( $symbols[ $currency ] )
            ? $symbols[ $currency ] . $value
            : $value . ' ' . $currency;
    }

    /**
     * A customer-facing one-line summary, e.g. "Includes VAT (20%): 4.17".
     *
     * @param array $breakdown Result of breakdown().
     * @return string
     */
    public static function summary_line( array $breakdown ) {
        if ( empty( $breakdown['rate'] ) || $breakdown['tax'] <= 0 ) {
            return '';
        }

        $rate = rtrim( rtrim( number_format( (float) $breakdown['rate'], 2 ), '0' ), '.' );

        if ( ! empty( $breakdown['inclusive'] ) ) {
            return sprintf(
                /* translators: 1: tax label e.g. VAT, 2: rate e.g. 20, 3: formatted amount. */
                __( 'Includes %1$s (%2$s%%): %3$s', 'yatoon-booking-system' ),
                self::label(),
                $rate,
                self::format_money( $breakdown['tax'] )
            );
        }

        return sprintf(
            /* translators: 1: tax label e.g. Sales Tax, 2: rate e.g. 8.25, 3: formatted amount. */
            __( '%1$s (%2$s%%): %3$s', 'yatoon-booking-system' ),
            self::label(),
            $rate,
            self::format_money( $breakdown['tax'] )
        );
    }
}
