<?php
/**
 * Session layer for guest wishlists.
 *
 * @package SKYNSMFA_Smart_Favorites
 */

declare( strict_types=1 );

namespace SKYNSMFA\Wishlist;

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


final class Session {
	/**
	 * Singleton instance.
	 *
	 * @var Session|null
	 */
	private static $instance = null;

	/**
	 * Session key for owner token.
	 *
	 * @var string
	 */
	private $owner_key = 'skynsmfa_owner';

	/**
	 * Session key for wishlist data.
	 *
	 * @var string
	 */
	private $data_key = 'skynsmfa_wishlist_data';

	/**
	 * Session key for last list used when adding from catalog (shop/single).
	 *
	 * @var string
	 */
	private $last_list_key = 'skynsmfa_last_wishlist_key';

	/**
	 * Session key for custom list display names (key => label).
	 *
	 * @var string
	 */
	private $names_key = 'skynsmfa_wishlist_names';

	/**
	 * Cached owner.
	 *
	 * @var string|null
	 */
	private $cached_owner = null;

	/**
	 * Get singleton instance.
	 *
	 * @return Session
	 */
	public static function instance(): Session {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Constructor.
	 */
	private function __construct() {
		add_action( 'woocommerce_init', array( $this, 'maybe_init_session' ) );
	}

	/**
	 * Ensure WC session exists and owner is set.
	 *
	 * @return void
	 */
	public function maybe_init_session(): void {
		if ( ! function_exists( 'WC' ) || ! WC()->session ) {
			return;
		}

		$this->get_owner();
	}

	/**
	 * Get session owner token (guest) or user-based token.
	 *
	 * @return string
	 */
	public function get_owner(): string {
		if ( null !== $this->cached_owner ) {
			return $this->cached_owner;
		}

		if ( is_user_logged_in() ) {
			$this->cached_owner = 'user:' . (string) get_current_user_id();
			return $this->cached_owner;
		}

		if ( ! function_exists( 'WC' ) || ! WC()->session ) {
			$this->cached_owner = 'guest:0';
			return $this->cached_owner;
		}

		$owner = (string) WC()->session->get( $this->owner_key, '' );

		if ( '' === $owner ) {
			$owner = 'guest:' . wp_generate_password( 20, false, false );
			WC()->session->set( $this->owner_key, $owner );
		}

		$this->cached_owner = $owner;
		return $this->cached_owner;
	}

	/**
	 * Get wishlists data from session.
	 *
	 * Format:
	 * [
	 *   'default' => [ product_id => [ 'product_id' => int, 'qty' => int, 'variation_id' => int ] ],
	 *   'custom:<id>' => [ ... ],
	 * ]
	 *
	 * @return array<string, array<int|string, array<string, int>>>
	 */
	public function get_data(): array {
		if ( ! function_exists( 'WC' ) || ! WC()->session ) {
			return array();
		}

		$data = WC()->session->get( $this->data_key, array() );
		return is_array( $data ) ? $data : array();
	}

	/**
	 * Persist wishlists data in session.
	 *
	 * @param array $data Data to store.
	 * @return void
	 */
	public function set_data( array $data ): void {
		if ( ! function_exists( 'WC' ) || ! WC()->session ) {
			return;
		}

		WC()->session->set( $this->data_key, $data );
	}

	/**
	 * Add a product to a session wishlist.
	 *
	 * @param int    $product_id Product ID.
	 * @param int    $qty Quantity.
	 * @param string $wishlist_key Wishlist key.
	 * @param int    $variation_id Variation ID or 0.
	 * @return array Updated data.
	 */
	public function add_item( int $product_id, int $qty = 1, string $wishlist_key = 'default', int $variation_id = 0 ): array {
		$data = $this->get_data();

		if ( ! isset( $data[ $wishlist_key ] ) || ! is_array( $data[ $wishlist_key ] ) ) {
			$data[ $wishlist_key ] = array();
		}

		$current_qty       = 0;
		$current_variation = 0;
		if ( isset( $data[ $wishlist_key ][ $product_id ] ) && is_array( $data[ $wishlist_key ][ $product_id ] ) ) {
			$current_qty       = absint( $data[ $wishlist_key ][ $product_id ]['qty'] ?? 0 );
			$current_variation = absint( $data[ $wishlist_key ][ $product_id ]['variation_id'] ?? 0 );
		}

		$new_variation = max( 0, $variation_id );
		$is_update     = isset( $data[ $wishlist_key ][ $product_id ] )
			&& $new_variation > 0
			&& $new_variation !== $current_variation;

		$data[ $wishlist_key ][ $product_id ] = array(
			'product_id'   => $product_id,
			'variation_id' => $new_variation,
			'qty'          => $is_update ? max( 1, $current_qty ) : max( 1, $current_qty + $qty ),
		);

		$this->set_data( $data );

		$this->log_analytics( 'add', $product_id );

		return $data;
	}

	/**
	 * Remove a product from a session wishlist.
	 *
	 * @param int    $product_id Product ID.
	 * @param string $wishlist_key Wishlist key.
	 * @return array Updated data.
	 */
	public function remove_item( int $product_id, string $wishlist_key = 'default' ): array {
		$data = $this->get_data();

		if ( isset( $data[ $wishlist_key ][ $product_id ] ) ) {
			unset( $data[ $wishlist_key ][ $product_id ] );
		}

		$this->set_data( $data );

		$this->log_analytics( 'remove', $product_id );

		return $data;
	}

	/**
	 * Stored display names for wishlist keys.
	 *
	 * @return array<string, string>
	 */
	public function get_list_display_names(): array {
		if ( ! function_exists( 'WC' ) || ! WC()->session ) {
			return array();
		}

		$names = WC()->session->get( $this->names_key, array() );
		if ( ! is_array( $names ) ) {
			return array();
		}

		$out = array();
		foreach ( $names as $key => $label ) {
			if ( ! is_string( $key ) || ! is_string( $label ) ) {
				continue;
			}
			$key   = sanitize_text_field( $key );
			$label = sanitize_text_field( $label );
			if ( '' === $key || '' === $label ) {
				continue;
			}
			$out[ $key ] = $label;
		}

		return $out;
	}

	/**
	 * Persist a display name for a wishlist key.
	 *
	 * @param string $wishlist_key Wishlist key.
	 * @param string $list_name Display name.
	 * @return void
	 */
	public function set_list_display_name( string $wishlist_key, string $list_name ): void {
		if ( ! function_exists( 'WC' ) || ! WC()->session ) {
			return;
		}

		$wishlist_key = sanitize_text_field( $wishlist_key );
		$list_name    = sanitize_text_field( $list_name );

		if ( '' === $wishlist_key || '' === $list_name ) {
			return;
		}

		$names                 = $this->get_list_display_names();
		$names[ $wishlist_key ] = $list_name;
		WC()->session->set( $this->names_key, $names );
	}

	/**
	 * Remove a stored display name.
	 *
	 * @param string $wishlist_key Wishlist key.
	 * @return void
	 */
	public function unset_list_display_name( string $wishlist_key ): void {
		if ( ! function_exists( 'WC' ) || ! WC()->session ) {
			return;
		}

		$wishlist_key = sanitize_text_field( $wishlist_key );
		if ( '' === $wishlist_key ) {
			return;
		}

		$names = $this->get_list_display_names();
		if ( ! isset( $names[ $wishlist_key ] ) ) {
			return;
		}

		unset( $names[ $wishlist_key ] );
		WC()->session->set( $this->names_key, $names );
	}

	/**
	 * Human-readable list label for a wishlist key.
	 *
	 * @param string $wishlist_key Wishlist key.
	 * @return string
	 */
	public function get_list_display_name( string $wishlist_key ): string {
		$wishlist_key = sanitize_text_field( $wishlist_key );

		if ( '' === $wishlist_key ) {
			return '';
		}

		$names = $this->get_list_display_names();
		if ( isset( $names[ $wishlist_key ] ) && '' !== $names[ $wishlist_key ] ) {
			return $names[ $wishlist_key ];
		}

		if ( 'default' === $wishlist_key ) {
			return __( 'My favorites', 'skynet-smart-favorites' );
		}

		$derived = str_replace( array( 'custom:', '-' ), array( '', ' ' ), $wishlist_key );
		return ucfirst( $derived );
	}

	/**
	 * Whether a display name is already used by another list.
	 *
	 * @param string $list_name Proposed name.
	 * @param string $exclude_key Optional key to ignore (when renaming).
	 * @return bool
	 */
	public function list_display_name_exists( string $list_name, string $exclude_key = '' ): bool {
		$list_name    = sanitize_text_field( $list_name );
		$exclude_key  = sanitize_text_field( $exclude_key );
		$target       = strtolower( trim( $list_name ) );

		if ( '' === $target ) {
			return false;
		}

		$data = $this->get_data();
		foreach ( array_keys( $data ) as $key ) {
			if ( ! is_string( $key ) || '' === $key ) {
				continue;
			}
			if ( '' !== $exclude_key && $exclude_key === $key ) {
				continue;
			}
			$existing = strtolower( trim( $this->get_list_display_name( $key ) ) );
			if ( $existing === $target ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Rename a wishlist list (updates display name; custom lists may get a new key).
	 *
	 * @param string $wishlist_key Current wishlist key.
	 * @param string $list_name New display name.
	 * @return array{key:string,name:string,data:array<string, array<int|string, array<string, int>>>}|null
	 */
	public function rename_list( string $wishlist_key, string $list_name ): ?array {
		$wishlist_key = sanitize_text_field( $wishlist_key );
		$list_name    = sanitize_text_field( $list_name );

		if ( '' === $wishlist_key || '' === $list_name ) {
			return null;
		}

		$data = $this->get_data();
		if ( ! isset( $data[ $wishlist_key ] ) || ! is_array( $data[ $wishlist_key ] ) ) {
			return null;
		}

		if ( 'default' === $wishlist_key ) {
			$this->set_list_display_name( 'default', $list_name );

			return array(
				'key'  => 'default',
				'name' => $list_name,
				'data' => $data,
			);
		}

		$base_key = 'custom:' . sanitize_title( $list_name );
		$new_key  = $base_key;
		$index    = 1;

		while ( isset( $data[ $new_key ] ) && $new_key !== $wishlist_key ) {
			$new_key = $base_key . '-' . $index;
			$index++;
		}

		if ( $new_key !== $wishlist_key ) {
			$data[ $new_key ] = $data[ $wishlist_key ];
			unset( $data[ $wishlist_key ] );
			$this->set_data( $data );
			$this->unset_list_display_name( $wishlist_key );

			if ( function_exists( 'WC' ) && WC()->session ) {
				$stored = WC()->session->get( $this->last_list_key, '' );
				if ( is_string( $stored ) && $stored === $wishlist_key ) {
					WC()->session->set( $this->last_list_key, $new_key );
				}
			}
		}

		$this->set_list_display_name( $new_key, $list_name );

		return array(
			'key'  => $new_key,
			'name' => $list_name,
			'data' => $data,
		);
	}

	/**
	 * Delete a custom wishlist list and its items.
	 *
	 * @param string $wishlist_key Wishlist key (must not be "default").
	 * @return array Updated data.
	 */
	public function delete_list( string $wishlist_key ): array {
		$wishlist_key = sanitize_text_field( $wishlist_key );

		if ( '' === $wishlist_key || 'default' === $wishlist_key ) {
			return $this->get_data();
		}

		$data = $this->get_data();

		if ( ! isset( $data[ $wishlist_key ] ) ) {
			return $data;
		}

		unset( $data[ $wishlist_key ] );
		$this->set_data( $data );
		$this->unset_list_display_name( $wishlist_key );

		if ( function_exists( 'WC' ) && WC()->session ) {
			$stored = WC()->session->get( $this->last_list_key, '' );
			if ( is_string( $stored ) && $stored === $wishlist_key ) {
				WC()->session->set( $this->last_list_key, 'default' );
			}
		}

		return $data;
	}

	/**
	 * Which list key holds this product for removal (preferred list first), or empty string.
	 *
	 * @param int    $product_id Product ID.
	 * @param string $preferred_key Preferred wishlist key (e.g. button context).
	 * @return string
	 */
	public function resolve_remove_list_key( int $product_id, string $preferred_key = 'default' ): string {
		$data = $this->get_data();

		if ( isset( $data[ $preferred_key ][ $product_id ] ) ) {
			return $preferred_key;
		}

		foreach ( $this->get_ordered_list_keys( $data ) as $key ) {
			if ( isset( $data[ $key ][ $product_id ] ) ) {
				return $key;
			}
		}

		return '';
	}

	/**
	 * Ordered list keys: default first, then others sorted for stable scans.
	 *
	 * @param array<string, mixed> $data Wishlist session data.
	 * @return array<int, string>
	 */
	private function get_ordered_list_keys( array $data ): array {
		$keys = array_keys( $data );
		$keys = array_filter(
			$keys,
			static function ( $k ): bool {
				return is_string( $k ) && '' !== $k;
			}
		);
		$keys = array_values( $keys );
		usort(
			$keys,
			static function ( string $a, string $b ): int {
				if ( 'default' === $a ) {
					return -1;
				}
				if ( 'default' === $b ) {
					return 1;
				}
				return strcmp( $a, $b );
			}
		);
		return $keys;
	}

	/**
	 * Last wishlist key used for catalog adds, validated against current data.
	 *
	 * @return string
	 */
	public function get_last_wishlist_key(): string {
		if ( ! function_exists( 'WC' ) || ! WC()->session ) {
			return 'default';
		}

		$stored = WC()->session->get( $this->last_list_key, '' );
		$stored = is_string( $stored ) ? $stored : '';

		$data = $this->get_data();
		if ( '' !== $stored && isset( $data[ $stored ] ) && is_array( $data[ $stored ] ) ) {
			return $stored;
		}

		if ( isset( $data['default'] ) && is_array( $data['default'] ) ) {
			return 'default';
		}

		$keys = $this->get_ordered_list_keys( $data );
		if ( ! empty( $keys ) ) {
			return $keys[0];
		}

		return 'default';
	}

	/**
	 * Persist last wishlist key after a successful catalog add.
	 *
	 * @param string $wishlist_key Key.
	 * @return void
	 */
	public function set_last_wishlist_key( string $wishlist_key ): void {
		if ( ! function_exists( 'WC' ) || ! WC()->session ) {
			return;
		}

		$wishlist_key = sanitize_text_field( $wishlist_key );
		if ( '' === $wishlist_key ) {
			$wishlist_key = 'default';
		}

		WC()->session->set( $this->last_list_key, $wishlist_key );
	}

	/**
	 * Get variation id for a line item.
	 *
	 * @param int    $product_id Product ID.
	 * @param string $wishlist_key List key.
	 * @return int
	 */
	public function get_variation_id_for_item( int $product_id, string $wishlist_key = 'default' ): int {
		$data = $this->get_data();
		if ( ! isset( $data[ $wishlist_key ][ $product_id ] ) || ! is_array( $data[ $wishlist_key ][ $product_id ] ) ) {
			return 0;
		}

		return absint( $data[ $wishlist_key ][ $product_id ]['variation_id'] ?? 0 );
	}

	/**
	 * Record analytics event when table exists.
	 *
	 * @param string $type add|remove.
	 * @param int    $product_id Product ID.
	 * @return void
	 */
	private function log_analytics( string $type, int $product_id ): void {
		if ( ! Feature_Gate::can_use_analytics() || ! class_exists( Analytics::class ) ) {
			return;
		}

		$user_id = is_user_logged_in() ? get_current_user_id() : 0;
		Analytics::instance()->log_event( $type, $product_id, $user_id, $this->get_owner() );
	}

	/**
	 * Count saved wishlist products (unique across lists by default).
	 *
	 * @param string|null $wishlist_key Optional list key; null counts all lists.
	 * @return int
	 */
	public function get_item_count( ?string $wishlist_key = null ): int {
		return self::count_items_from_data( $this->get_data(), $wishlist_key );
	}

	/**
	 * Count wishlist products from session-shaped data.
	 *
	 * @param array<string, mixed> $data Wishlist data.
	 * @param string|null          $wishlist_key Optional list key; null counts all lists.
	 * @return int
	 */
	public static function count_items_from_data( array $data, ?string $wishlist_key = null ): int {
		if ( null !== $wishlist_key && '' !== $wishlist_key ) {
			if ( ! isset( $data[ $wishlist_key ] ) || ! is_array( $data[ $wishlist_key ] ) ) {
				return 0;
			}

			return count( $data[ $wishlist_key ] );
		}

		$product_ids = array();

		foreach ( $data as $list_items ) {
			if ( ! is_array( $list_items ) ) {
				continue;
			}

			foreach ( $list_items as $product_id => $item ) {
				unset( $item );
				$id = absint( $product_id );
				if ( $id > 0 ) {
					$product_ids[ $id ] = true;
				}
			}
		}

		return count( $product_ids );
	}
}
