<?php

declare(strict_types=1);

namespace SiteFort\Modules\Firewall\GeoIP;

use SiteFort\Includes\FilesystemMutation;
use SiteFort\Includes\GuardedDataFile;
use SiteFort\Includes\RemoteUrlValidator;

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

/**
 * MaxMind GeoLite2-Country database downloader.
 *
 * Downloads and extracts the GeoLite2-Country MMDB file from MaxMind using
 * a valid license key. The download is performed atomically (write to temp,
 * then rename) to avoid serving a partially-written database.
 *
 * Requires a MaxMind license key, which can be obtained for free by
 * registering at https://www.maxmind.com/en/geolite2/signup
 *
 */
class MaxMindDownloader {

    /**
     * MaxMind GeoLite2 download URL (current API, authenticates via HTTP Basic Auth).
     */
    private const DOWNLOAD_URL = 'https://download.maxmind.com/geoip/databases';

    /**
     * Directory where the MMDB file is stored.
     */
    private const DB_DIR = '/sitefort-data/geoip';

    /**
     * Filename of the MMDB database.
     */
    private const DB_FILENAME = 'GeoLite2-Country.mmdb.php';
    private const LEGACY_DB_FILENAME = 'GeoLite2-Country.mmdb';

    /**
     * Maximum age in seconds before the database needs updating (7 days).
     */
    private const MAX_AGE = 604800;

    /**
     * Download and install the GeoLite2-Country database.
     *
     * Performs the following steps:
     * 1. Downloads the tar.gz archive from MaxMind using wp download_url().
     * 2. Extracts the .mmdb file from the archive using PharData.
     * 3. Atomically moves the extracted file to the target location.
     *
     * @param string $account_id  MaxMind account ID.
     * @param string $license_key MaxMind license key.
     * @return true|\WP_Error True on success, WP_Error on failure.
     */
    public function download( string $account_id, string $license_key ) {
        if ( empty( $account_id ) || empty( $license_key ) ) {
            return new \WP_Error(
                'sitefort_geoip_no_key',
                'MaxMind account ID and license key are both required to download the GeoIP database.'
            );
        }

        $url = self::DOWNLOAD_URL . '/GeoLite2-Country/download?suffix=tar.gz';

        $tmp_file = $this->download_archive( $url, $account_id, $license_key );

        if ( is_wp_error( $tmp_file ) ) {
            return $tmp_file;
        }

        $target_dir = WP_CONTENT_DIR . self::DB_DIR;
        if ( ! is_dir( $target_dir ) ) {
            wp_mkdir_p( $target_dir );
        }

        // Extract the .mmdb file from the tar.gz archive.
        $mmdb_content = $this->extract_mmdb( $tmp_file );

        // Clean up the downloaded archive.
        wp_delete_file( $tmp_file );

        if ( is_wp_error( $mmdb_content ) ) {
            return $mmdb_content;
        }

        // Atomic write: write to a temp file, then rename.
        $target_path = $target_dir . '/' . self::DB_FILENAME;
        $temp_path   = $target_path . '.tmp.' . $this->temp_suffix();

        if ( ! GuardedDataFile::write_payload_atomic( $temp_path, $mmdb_content ) ) {
            wp_delete_file( $temp_path );
            return new \WP_Error(
                'sitefort_geoip_write_failed',
                'Failed to write GeoIP database to disk.'
            );
        }

        // Validate the written file contains the MMDB marker.
        $payload_offset = GuardedDataFile::payload_offset_for_path( $temp_path );
        $raw_size       = filesize( $temp_path );
        $payload_size   = ( false === $raw_size ) ? false : max( 0, (int) $raw_size - $payload_offset );

        $check = false;
        if ( false !== $payload_size ) {
            $initial_offset = max( 0, $payload_size - 131072 );
            $check = file_get_contents( $temp_path, false, null, $payload_offset + $initial_offset );
        }
        if ( $check === false || strpos( $check, "\xab\xcd\xefMaxMind.com" ) === false ) {
            // Try reading from the end more carefully.
            if ( $payload_size !== false && $payload_size > 128 ) {
                $check = file_get_contents( $temp_path, false, null, $payload_offset + max( 0, $payload_size - 131072 ) );
                if ( $check === false || strpos( $check, "\xab\xcd\xefMaxMind.com" ) === false ) {
                    wp_delete_file( $temp_path );
                    return new \WP_Error(
                        'sitefort_geoip_invalid',
                        'Downloaded file does not appear to be a valid MMDB database.'
                    );
                }
            } else {
                wp_delete_file( $temp_path );
                return new \WP_Error(
                    'sitefort_geoip_invalid',
                    'Downloaded file does not appear to be a valid MMDB database.'
                );
            }
        }

        // Promote the extracted database into place through the shared
        // filesystem mutation layer so all transports follow the same rules.
        if ( ! FilesystemMutation::move_path( $temp_path, $target_path ) ) {
            wp_delete_file( $temp_path );
            return new \WP_Error(
                'sitefort_geoip_rename_failed',
                'Failed to finalize GeoIP database installation.'
            );
        }

        // Store the download timestamp.
        \SiteFort\Includes\SettingsStore::set_field( \SiteFort\Includes\SettingsStore::COL_STATE, 'geoip_last_download', time() );

        return true;
    }

    /**
     * Get information about the current database file.
     *
     * @return array{exists: bool, last_modified: int|null, file_size: int|null, last_download: int|null}
     */
    public function get_db_info(): array {
        $guarded_path = WP_CONTENT_DIR . self::DB_DIR . '/' . self::DB_FILENAME;
        $legacy_path  = WP_CONTENT_DIR . self::DB_DIR . '/' . self::LEGACY_DB_FILENAME;
        $path         = is_file( $guarded_path ) ? $guarded_path : $legacy_path;

        $info = [
            'exists'        => false,
            'last_modified' => null,
            'file_size'     => null,
            'last_download' => \SiteFort\Includes\SettingsStore::get_field( \SiteFort\Includes\SettingsStore::COL_STATE, 'geoip_last_download', null ),
        ];

        if ( is_file( $path ) && is_readable( $path ) ) {
            $raw_size       = filesize( $path );
            $payload_offset = GuardedDataFile::payload_offset_for_path( $path );
            $info['exists']        = true;
            $info['last_modified'] = filemtime( $path ) ?: null;
            $info['file_size']     = ( false !== $raw_size ) ? max( 0, (int) $raw_size - $payload_offset ) : null;
        }

        return $info;
    }

    /**
     * Check if the database needs updating (older than 7 days).
     *
     * @return bool True if the database is missing or older than 7 days.
     */
    public function needs_update(): bool {
        $info = $this->get_db_info();

        if ( ! $info['exists'] ) {
            return true;
        }

        if ( $info['last_modified'] === null ) {
            return true;
        }

        return ( time() - $info['last_modified'] ) > self::MAX_AGE;
    }

    /**
     * Download a file via wp_remote_get() with HTTP Basic Auth and stream to a temp file.
     *
     * @param string $url         The download URL.
     * @param string $account_id  MaxMind account ID (Basic Auth username).
     * @param string $license_key MaxMind license key (Basic Auth password).
     * @return string|\WP_Error Path to the downloaded temp file, or WP_Error on failure.
     */
    private function download_archive( string $url, string $account_id, string $license_key ) {
        // Step 1: Authenticate with MaxMind (do NOT follow redirects).
        // MaxMind redirects to an S3 pre-signed URL. If we follow the redirect
        // with the Basic Auth header attached, S3 rejects it (missing x-amz-content-sha256).
        $auth_response = wp_remote_get( $url, [
            'timeout'     => 30,
            'redirection' => 0,
            'headers'     => [
                'Authorization' => 'Basic ' . base64_encode( $account_id . ':' . $license_key ),
            ],
        ] );

        if ( is_wp_error( $auth_response ) ) {
            return new \WP_Error(
                'sitefort_geoip_download_failed',
                'Failed to connect to MaxMind: ' . $auth_response->get_error_message()
            );
        }

        $code = wp_remote_retrieve_response_code( $auth_response );

        // 401/403: invalid credentials.
        if ( $code === 401 || $code === 403 ) {
            return new \WP_Error(
                'sitefort_geoip_auth_failed',
                'MaxMind authentication failed. Please verify your Account ID and License Key.'
            );
        }

        // Expect a 3xx redirect to the S3 download URL.
        $redirect_url = wp_remote_retrieve_header( $auth_response, 'location' );
        if ( empty( $redirect_url ) || $code < 300 || $code >= 400 ) {
            $body = wp_remote_retrieve_body( $auth_response );
            return new \WP_Error(
                'sitefort_geoip_download_failed',
                'Unexpected response from MaxMind (HTTP ' . $code . ').'
                . ( $body !== '' ? ' ' . substr( wp_strip_all_tags( $body ), 0, 200 ) : '' )
            );
        }
        $redirect_url = is_string( $redirect_url ) ? trim( $redirect_url ) : '';
        if ( ! RemoteUrlValidator::is_trusted_maxmind_redirect_url( $redirect_url ) ) {
            return new \WP_Error(
                'sitefort_geoip_untrusted_redirect',
                'MaxMind returned an untrusted GeoIP download URL.'
            );
        }

        // Step 2: Download the file from the redirect URL (no auth header).
        $tmpfname = wp_tempnam( 'GeoLite2-Country.tar.gz' );
        if ( ! $tmpfname ) {
            return new \WP_Error(
                'sitefort_geoip_download_failed',
                'Could not create temporary file for GeoIP download.'
            );
        }

        $download_args = [
            'timeout'  => 300,
            'stream'   => true,
            'filename' => $tmpfname,
            'reject_unsafe_urls' => true,
        ];
        $response = function_exists( 'wp_safe_remote_get' )
            ? wp_safe_remote_get( $redirect_url, $download_args )
            : wp_remote_get( $redirect_url, $download_args );

        if ( is_wp_error( $response ) ) {
            wp_delete_file( $tmpfname );
            return new \WP_Error(
                'sitefort_geoip_download_failed',
                'Failed to download GeoIP database: ' . $response->get_error_message()
            );
        }

        $dl_code = wp_remote_retrieve_response_code( $response );
        if ( 200 !== $dl_code ) {
            wp_delete_file( $tmpfname );
            return new \WP_Error(
                'sitefort_geoip_download_failed',
                'Download failed (HTTP ' . $dl_code . '). Please try again.'
            );
        }

        return $tmpfname;
    }

    /**
     * Extract the .mmdb file content from a tar.gz archive.
     *
     * Uses PharData to open and traverse the archive, searching for
     * a file ending in '.mmdb'.
     *
     * @param string $archive_path Path to the tar.gz file.
     * @return string|\WP_Error The raw MMDB file contents, or WP_Error on failure.
     */
    private function extract_mmdb( string $archive_path ) {
        if ( ! class_exists( '\PharData' ) ) {
            return new \WP_Error(
                'sitefort_geoip_no_phar',
                'PharData extension is required to extract the GeoIP database.'
            );
        }

        try {
            $phar = new \PharData( $archive_path );

            // Iterate through the archive to find the .mmdb file.
            foreach ( new \RecursiveIteratorIterator( $phar ) as $file ) {
                /** @var \PharFileInfo $file */
                if ( str_ends_with( $file->getFilename(), '.mmdb' ) ) {
                    $content = $file->getContent();
                    if ( $content !== '' ) {
                        return $content;
                    }
                }
            }

            return new \WP_Error(
                'sitefort_geoip_no_mmdb',
                'No .mmdb file found in the downloaded archive.'
            );
        } catch ( \Throwable $e ) {
            return new \WP_Error(
                'sitefort_geoip_extract_failed',
                'Failed to extract GeoIP database: ' . $e->getMessage()
            );
        }
    }

    /**
     * Generate a filesystem-safe temporary suffix.
     */
    private function temp_suffix(): string {
        try {
            return bin2hex( random_bytes( 6 ) );
        } catch ( \Throwable $e ) {
            return dechex( wp_rand( 0, PHP_INT_MAX ) );
        }
    }
}
