<?php
namespace R2_Offload\Storage;

defined( 'ABSPATH' ) || exit;

/**
 * AWS Signature Version 4 signer for S3-compatible APIs.
 *
 * Self-contained — no AWS SDK. Produces the headers (Authorization + the signed
 * x-amz-* headers) for a single request. Path-style addressing is used by the
 * caller, which keeps it compatible with every S3-clone (R2, B2, Wasabi, Spaces).
 */
final class S3_Signer {

	private string $access_key;
	private string $secret_key;
	private string $region;
	private string $service;

	public function __construct( string $access_key, string $secret_key, string $region, string $service = 's3' ) {
		$this->access_key = $access_key;
		$this->secret_key = $secret_key;
		$this->region     = $region !== '' ? $region : 'us-east-1';
		$this->service    = $service;
	}

	/**
	 * Sign a request.
	 *
	 * @param string               $method       HTTP verb.
	 * @param string               $url          Full URL (scheme://host/path?query). Path must be pre-encoded.
	 * @param array<string,string> $headers      Caller headers to include (e.g. content-type). Case-insensitive keys.
	 * @param string               $payload_hash Hex sha256 of the body (or 'UNSIGNED-PAYLOAD').
	 * @param string               $amz_date     ISO8601 basic (YYYYMMDDTHHMMSSZ).
	 * @return array<string,string> Full header set ready for the HTTP request.
	 */
	public function sign( string $method, string $url, array $headers, string $payload_hash, string $amz_date ): array {
		$parts = wp_parse_url( $url );
		$host  = (string) ( $parts['host'] ?? '' );
		if ( isset( $parts['port'] ) ) {
			$host .= ':' . $parts['port'];
		}
		$path  = (string) ( $parts['path'] ?? '/' );
		$query = (string) ( $parts['query'] ?? '' );
		$date  = substr( $amz_date, 0, 8 );

		// Assemble the headers that participate in the signature.
		$signed = array_change_key_case( $headers, CASE_LOWER );
		$signed['host']                 = $host;
		$signed['x-amz-content-sha256'] = $payload_hash;
		$signed['x-amz-date']           = $amz_date;
		ksort( $signed );

		$canonical_headers = '';
		$signed_names      = [];
		foreach ( $signed as $name => $value ) {
			$canonical_headers .= $name . ':' . self::trim_header( (string) $value ) . "\n";
			$signed_names[]     = $name;
		}
		$signed_headers_list = implode( ';', $signed_names );

		$canonical_request = implode(
			"\n",
			[
				strtoupper( $method ),
				$path === '' ? '/' : $path,
				self::canonical_query( $query ),
				$canonical_headers,
				$signed_headers_list,
				$payload_hash,
			]
		);

		$scope         = $date . '/' . $this->region . '/' . $this->service . '/aws4_request';
		$string_to_sign = implode(
			"\n",
			[
				'AWS4-HMAC-SHA256',
				$amz_date,
				$scope,
				hash( 'sha256', $canonical_request ),
			]
		);

		$signing_key = $this->signing_key( $date );
		$signature   = hash_hmac( 'sha256', $string_to_sign, $signing_key );

		$authorization = sprintf(
			'AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s',
			$this->access_key,
			$scope,
			$signed_headers_list,
			$signature
		);

		$signed['authorization'] = $authorization;
		return $signed;
	}

	private function signing_key( string $date ): string {
		$k_date    = hash_hmac( 'sha256', $date, 'AWS4' . $this->secret_key, true );
		$k_region  = hash_hmac( 'sha256', $this->region, $k_date, true );
		$k_service = hash_hmac( 'sha256', $this->service, $k_region, true );
		return hash_hmac( 'sha256', 'aws4_request', $k_service, true );
	}

	private static function trim_header( string $value ): string {
		// Collapse internal whitespace runs, trim ends (per SigV4 spec).
		return trim( (string) preg_replace( '/\s+/', ' ', $value ) );
	}

	private static function canonical_query( string $query ): string {
		if ( $query === '' ) {
			return '';
		}
		$pairs = [];
		foreach ( explode( '&', $query ) as $pair ) {
			if ( $pair === '' ) {
				continue;
			}
			[ $k, $v ] = array_pad( explode( '=', $pair, 2 ), 2, '' );
			$pairs[ rawurlencode( rawurldecode( $k ) ) ] = rawurlencode( rawurldecode( $v ) );
		}
		ksort( $pairs );
		$out = [];
		foreach ( $pairs as $k => $v ) {
			$out[] = $k . '=' . $v;
		}
		return implode( '&', $out );
	}

	/** Percent-encode an object key for use in a path, preserving '/' separators. */
	public static function encode_key( string $key ): string {
		$segments = explode( '/', $key );
		$segments = array_map( 'rawurlencode', $segments );
		return implode( '/', $segments );
	}
}
