<?php

namespace Twnt4All\Engine;

use Twnt4All\Support\WooBridge;

/**
 * Class Helper
 * @package Twnt4All\Engine
 */
class EnvironmentConfig
{
    /**
     * Returns the Cash Register ID. When no value has been saved yet, derives one
     * from the WordPress site title (stripped of accents and special characters)
     * and persists it so TWINT receives a stable identifier.
     *
     * @return string
     */
    public static function get_cash_register_id()
    {
        $cash_register_id = get_option( 't4a_twint_settings_registerid' );

        if ( empty( $cash_register_id ) ) {
            $cash_register_id = self::default_register_id_from_site();
            update_option( 't4a_twint_settings_registerid', $cash_register_id );
        }

        return $cash_register_id;
    }

    /**
     * Builds a default register identifier from the site title:
     * accents stripped, non-alphanumeric characters removed, capped at 50 chars.
     */
    protected static function default_register_id_from_site()
    {
        $title = function_exists( 'get_bloginfo' ) ? get_bloginfo( 'name' ) : '';
        if ( function_exists( 'remove_accents' ) ) {
            $title = remove_accents( $title );
        }
        $clean = preg_replace( '/[^A-Za-z0-9]/', '', (string) $title );
        $clean = substr( $clean, 0, 50 );
        return $clean !== '' ? $clean : 'Register1';
    }

    /**
     * Returns the saved certificate password. The value is stored
     * encrypted via Cipher (authenticated encryption). Legacy installs
     * that still hold the password in cleartext (pre-2.4) are handled
     * transparently — we try to decrypt first, fall back to the raw
     * value + stripcslashes() shape used historically.
     *
     * @return string
     */
    public static function get_certificate_password()
    {
        $raw = (string) get_option( 't4a_twint_settings_certpw', '' );
        if ( $raw === '' ) { return ''; }
        // Only attempt Cipher::decrypt when the stored value starts
        // with one of the tag prefixes emitted by our new encrypt() :
        // `S1:` (sodium) or `G1:` (aes-gcm). Any other shape is a
        // legacy plaintext (pre-2.4 addcslashes-encoded) and MUST go
        // straight to the legacy branch — otherwise we'd fall into
        // decrypt_legacy_ctr, which for some plaintexts may produce
        // garbage bytes indistinguishable from a « successful »
        // decryption and return that garbage to the SoapClient,
        // silently breaking the mTLS handshake with TWINT.
        if ( strncmp( $raw, 'S1:', 3 ) === 0 || strncmp( $raw, 'G1:', 3 ) === 0 ) {
            try {
                $decrypted = ( new \Twnt4All\Support\Cipher() )->decrypt( $raw );
                if ( is_string( $decrypted ) && $decrypted !== '' ) {
                    return $decrypted;
                }
            } catch ( \Throwable $e ) {
                // Encrypted format but decryption failed — fall through
                // to the legacy plaintext branch as a last-ditch effort.
            }
        }
        return stripcslashes( $raw );
    }

    /**
     * Persists the certificate password encrypted at rest. Called by
     * every settings-save handler (AJAX + form POST + wizard). Empty
     * input is normalised to an empty option so the config page's
     * « leave blank to keep current » UX works correctly.
     */
    public static function save_certificate_password( string $plain ) : void
    {
        if ( $plain === '' ) { return; }
        try {
            $enc = ( new \Twnt4All\Support\Cipher() )->encrypt( $plain );
            update_option( 't4a_twint_settings_certpw', $enc );
        } catch ( \Throwable $e ) {
            // Very unlikely (Cipher only fails when LOGGED_IN_KEY /
            // LOGGED_IN_SALT are missing — WP would be unusable) but
            // don't lose the password if it happens.
            update_option( 't4a_twint_settings_certpw', $plain );
        }
    }

    /**
     * Returns the Merchant UUID or false if it doesn't exist.
     *
     * @return string|bool
     */
    public static function get_merchant_uuid()
    {
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- T4A_PREFIX equals the plugin's internal prefix (`t4a_twint`) ; the sniffer cannot statically resolve the concatenation.
        return apply_filters( T4A_PREFIX . '_soap_merchant_uuid', get_option( 't4a_twint_settings_uuid' ) );
    }

    /**
     * Returns the connection timeout.
     *
     * @return int
     */
    public static function get_connection_timeout()
    {
        $connection_timeout = get_option( 't4a_twint_settings_connection_timeout' );
        if ( empty( $connection_timeout ) ) {
            $connection_timeout = T4A_DEFAULT_CONNECTION_TIMEOUT;
        }

        return $connection_timeout;
    }

    /**
     * Returns the TWINT uploads directory path and creates it if it doesn't exist yet.
     * Includes trailing slash.
     *
     * @return string
     */
    public static function get_uploads_dir()
    {
        $twint_upload_dir = wp_upload_dir()[ 'basedir' ] . DIRECTORY_SEPARATOR . 't4a_twint' . DIRECTORY_SEPARATOR;

        if ( !file_exists( $twint_upload_dir ) ) {
            wp_mkdir_p( $twint_upload_dir );
        }
        // Double protection : `.htaccess` for Apache + empty `index.php`
        // so nginx (which silently ignores `.htaccess`) can't serve the
        // certificate directly. The audit specifically flagged that the
        // htaccess-only setup was a no-op on the majority of shared
        // hosts. Written every call — cheap file_exists() short-circuit
        // means we only touch disk on the first bootstrap.
        $ht = $twint_upload_dir . '.htaccess';
        if ( ! file_exists( $ht ) ) {
            file_put_contents( $ht, "Order allow,deny\nDeny from all\nRequire all denied\n" );
        }
        $idx = $twint_upload_dir . 'index.php';
        if ( ! file_exists( $idx ) ) {
            file_put_contents( $idx, "<?php\n// Silence is golden.\n" );
        }

        return $twint_upload_dir;
    }

    /**
     * Random, per-install filename for the merchant's TWINT PEM. Minted
     * on first save and cached in wp_options ; falls back to the legacy
     * `twint.pem` for installs that predate this change so old certs
     * keep working until the merchant uploads a new one.
     *
     * The random name (48 hex chars ~ 192 bits) makes the file
     * unguessable ; combined with the .htaccess + index.php in the
     * folder it is no longer trivially fetchable by URL on nginx.
     */
    public static function get_certificate_filename() : string
    {
        $name = (string) get_option( 't4a_twint_settings_certfile', '' );
        if ( $name !== '' && preg_match( '/^[a-f0-9]{16,}\.pem$/i', $name ) ) {
            return $name;
        }
        // Fallback : the historical constant name — only used until the
        // merchant re-saves a certificate. `T4A_CERTIFICATE_NAME` is
        // kept for backward compatibility with older callers.
        return defined( 'T4A_CERTIFICATE_NAME' ) ? T4A_CERTIFICATE_NAME : 'twint.pem';
    }

    private static function mint_certificate_filename() : string
    {
        $name = bin2hex( random_bytes( 24 ) ) . '.pem';
        update_option( 't4a_twint_settings_certfile', $name, false );
        return $name;
    }

    /**
     * Returns the directory for lock files.
     * Includes trailing slash.
     *
     * @return string
     */
    public static function get_locks_dir()
    {
        $locks_dir = static::get_uploads_dir() . 'locks' . DIRECTORY_SEPARATOR;

        if ( !file_exists( $locks_dir ) ) {
            wp_mkdir_p( $locks_dir );
            // Empty index.php to prevent directory listing. WordPress.org
            // requires the file to be empty (writing any PHP code — even a
            // comment — is treated as a code file and rejected).
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- one-time bootstrap of an empty silence file for a plugin-owned uploads subfolder.
            file_put_contents( $locks_dir . 'index.php', '' );
        }

        return $locks_dir;
    }

    public static function save_certificate_file( $file )
    {
        $dir = EnvironmentConfig::get_uploads_dir();

        // Mint a fresh random filename on every upload so a previously
        // exposed URL is invalidated. Also cleans up the historical
        // `twint.pem` if it lingers from an install that predates the
        // random-name change.
        $legacy_name = defined( 'T4A_CERTIFICATE_NAME' ) ? T4A_CERTIFICATE_NAME : 'twint.pem';
        $legacy      = $dir . $legacy_name;
        if ( file_exists( $legacy ) ) {
            wp_delete_file( $legacy );
        }
        $old_name = (string) get_option( 't4a_twint_settings_certfile', '' );
        if ( $old_name !== '' && $old_name !== $legacy_name && file_exists( $dir . $old_name ) ) {
            wp_delete_file( $dir . $old_name );
        }

        $new_name = self::mint_certificate_filename();
        $path     = $dir . $new_name;
        $ok       = file_put_contents( $path, $file );
        if ( $ok !== false && file_exists( $path ) ) {
            // Restrict file mode so a shared-host neighbour reading the
            // filesystem still can't get to the private key. WP_Filesystem
            // is initialised here explicitly ; get_filesystem_method() must
            // be 'direct' for chmod to be meaningful (SSH/FTP hosts wrap
            // the same call, so we still get the intended permission bits).
            if ( ! function_exists( 'WP_Filesystem' ) ) {
                require_once ABSPATH . 'wp-admin/includes/file.php';
            }
            global $wp_filesystem;
            if ( WP_Filesystem() && $wp_filesystem ) {
                $wp_filesystem->chmod( $path, 0600 );
            }
        }
        return $ok;
    }

    /**
     * Returns the path to the certificate file.
     *
     * @return string
     */
    public static function get_certificate_file_path()
    {
        return EnvironmentConfig::get_uploads_dir() . self::get_certificate_filename();
    }

    /**
     * Returns true if the WC order is in a complete state.
     * on-hold for deferred. processing or completed for non-deferred.
     *
     * @param $order
     * @param bool $deferred
     * @return bool
     */
    public static function is_order_complete( $order, $deferred = false )
    {
        // Guard: WooBridge lives in pay4all-pro (WC-only). Without it the
        // notion of "complete WC order" doesn't apply.
        if ( ! class_exists( '\\Twnt4All\\Support\\WooBridge' ) ) {
            return false;
        }
        $wc_order_status   = WooBridge::get_order_status( $order );
        $complete_statuses = [ 'processing', 'completed' ];
        if ( $deferred ) {
            $complete_statuses[] = 'on-hold';
        }

        return in_array( $wc_order_status, $complete_statuses );
    }
}
