<?php

namespace Limb_Chatbot\Includes\Data_Objects;

use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Users_Controller;

/**
 * Class Chatbot_User
 *
 * Represents a user interacting with the chatbot system,
 * supporting both guest users and WordPress authenticated users.
 *
 * @package Limb_Chatbot\Includes\Data_Objects
 * @since 1.0.0
 */
class Chatbot_User extends WPDB_Data_Object {

	/**
	 * Database table name.
	 */
	const TABLE_NAME = 'lbaic_users';

	/**
	 * Attributes allowed for mass assignment.
	 *
	 * @var array
	 */
	const FILLABLE = [ 'id', 'uuid', 'wp_user_id', 'device_uuid', 'ip', 'type', 'status', 'created_at', 'updated_at', ];

	/**
	 * User type: guest.
	 */
	const TYPE_GUEST = 1;


	/**
	 * WordPress role name for agents.
	 *
	 * @var string
	 * @since 1.0.0
	 */
	const ROLE_AGENT = 'agent';

	/**
	 * User type: WordPress registered user.
	 */
	const TYPE_WP_USER = 2;

	/**
	 * Status active
	 */
	const STATUS_ACTIVE = 1;

	/**
	 * Status inactive
	 */
	const STATUS_INACTIVE = 2;

	/**
	 * User type: Live agent
	 */
	const TYPE_AGENT = 3;

	/**
	 * Database record ID.
	 *
	 * @var int|null
	 */
	public ?int $id;

	/**
	 * Unique user UUID.
	 *
	 * @var string
	 */
	public string $uuid;

	/**
	 * IP address of the user.
	 *
	 * @var string|null
	 */
	public ?string $ip = null;

	/**
	 * Associated WordPress user ID (if applicable).
	 *
	 * @var int|null
	 */
	public ?int $wp_user_id = null;

	/**
	 * Status of user (mainly for agents)
	 *
	 * @var int|null
	 */
	public ?int $status = null;

	/**
	 * Device UUID for tracking.
	 *
	 * @var string|null
	 */
	public ?string $device_uuid = null;

	/**
	 * Timestamp of record creation.
	 *
	 * @var string
	 */
	public string $created_at;

	/**
	 * User type identifier.
	 *
	 * @var int|null
	 */
	public ?int $type;

	/**
	 * Timestamp of last update.
	 *
	 * @var string|null
	 */
	public ?string $updated_at = null;

	/**
	 * Array of included related objects.
	 * @json_excluded
	 *
	 * @var array
	 * @since 1.0.0
	 */
	public array $included = [];

	/**
	 * Meta properties related to this chat.
	 *
	 * @var array
	 * @since 1.0.0
	 */
	protected array $meta_properties = [
		'name',
		'email',
		'location',
		'agent',
		'wp_user',
		'type_label',
		'country',
		'geolocation',
		'device_info',
	];

	/**
	 * Find a Chatbot_User by UUID.
	 *
	 * @param  string  $uuid  UUID string to search for.
	 *
	 * @return static|null The Chatbot_User instance if found, null otherwise.
	 * @since 1.0.0
	 */
	public static function find_by_uuid( string $uuid ) {
		return self::where( [ 'uuid' => $uuid ] )->first();
	}

	/**
	 * Get the device UUID.
	 *
	 * @return string|null Device UUID string or null if not set.
	 * @since 1.0.0
	 */
	public function get_device_uuid(): ?string {
		return $this->device_uuid;
	}

	/**
	 * Set the device UUID.
	 *
	 * @param  string|null  $device_uuid  Device UUID string or null to unset.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_device_uuid( ?string $device_uuid ): void {
		$this->device_uuid = $device_uuid;
	}

	/**
	 * Get the user UUID.
	 *
	 * @return string UUID string.
	 * @since 1.0.0
	 */
	public function get_uuid(): string {
		return $this->uuid;
	}

	/**
	 * Set the user UUID.
	 *
	 * @param  string  $uuid  UUID string.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_uuid( string $uuid ): void {
		$this->uuid = $uuid;
	}

	/**
	 * Get the creation timestamp.
	 *
	 * @return string Timestamp string.
	 * @since 1.0.0
	 */
	public function get_created_at(): string {
		return $this->created_at;
	}

	/**
	 * Set the creation timestamp.
	 *
	 * @param  string  $created_at  Timestamp string.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_created_at( string $created_at ): void {
		$this->created_at = $created_at;
	}

	/**
	 * Get the last updated timestamp.
	 *
	 * @return string|null Timestamp string or null if never updated.
	 * @since 1.0.0
	 */
	public function get_updated_at(): ?string {
		return $this->updated_at;
	}

	/**
	 * Set the last updated timestamp.
	 *
	 * @param  string|null  $updated_at  Timestamp string or null.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_updated_at( ?string $updated_at ): void {
		$this->updated_at = $updated_at;
	}

	/**
	 * Increment the usage count of new chat creation for this user.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function increment_new_chat_usage(): void {
		$current_usage = $this->get_meta( 'current_new_chat_usage' );
		$current_usage = $current_usage ? $current_usage + 1 : 1;
		$this->update_meta( 'current_new_chat_usage', $current_usage );
	}

	/**
	 * Get a meta value by key for this user.
	 *
	 * @param  string  $string  Meta key name.
	 *
	 * @return mixed|null Meta value or null if not set.
	 * @since 1.0.0
	 */
	public function get_meta( string $string ) {
		$meta = Chatbot_User_Meta::where( [ 'chatbot_user_id' => $this->get_id(), 'meta_key' => $string ] )->first();

		return $meta ? $meta->get_meta_value() : null;
	}

	/**
	 * Get the user ID.
	 *
	 * @return int|null User ID or null if not set.
	 * @since 1.0.0
	 */
	public function get_id(): ?int {
		return $this->id;
	}

	/**
	 * Set the user ID.
	 *
	 * @param  int|null  $id  User ID.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_id( $id ): void {
		$this->id = $id;
	}

	/**
	 * Update a meta key-value pair for this user.
	 *
	 * @param  string  $meta_key  Meta key name.
	 * @param  mixed  $value  Meta value.
	 *
	 * @return Chatbot_User_Meta|WPDB_Data_Object
	 * @since 1.0.0
	 */
	public function update_meta( $meta_key, $value ) {
		return Chatbot_User_Meta::update( [ 'meta_key' => $meta_key, 'chatbot_user_id' => $this->get_id() ],
			[ 'meta_value' => $value ] );
	}

	/**
	 * Get the user type.
	 *
	 * @return int|null User type constant.
	 * @since 1.0.0
	 */
	public function get_type(): ?int {
		return $this->type;
	}

	/**
	 * Set the user type.
	 *
	 * @param  int|null  $type  User type constant.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_type( ?int $type ): void {
		$this->type = $type;
	}

	/**
	 * Check if the current logged-in user has a specific capability,
	 * and matches this chatbot user.
	 *
	 * @param  string  $cap  Capability name.
	 *
	 * @return bool True if the user can perform the capability, false otherwise.
	 * @since 1.0.0
	 */
	public function wp_can( $cap ): bool {
		return $this->is_wp_user() && get_current_user_id() === $this->wp_user_id && current_user_can( $cap );
	}

	/**
	 * Determine if this user is a WordPress registered user.
	 *
	 * @return bool True if the user is a WordPress registered user, false otherwise.
	 * @since 1.0.0
	 */
	public function is_wp_user(): bool {
		return (bool) $this->wp_user_id;
	}

	/**
	 * Get the current user status.
	 *
	 * @return int|null
	 */
	public function get_status(): ?int {
		return $this->status;
	}

	/**
	 * Set the current user status.
	 *
	 * @param  int|null  $status
	 *
	 * @return void
	 */
	public function set_status( ?int $status ): void {
		$this->status = $status;
	}

	/**
	 * Return the name of the chatbot user
	 *
	 * @return mixed|string|null
	 * @since 1.0.0
	 */
	public function name() {
		if ( $this->is_wp_user() ) {
			$user = get_user_by( 'id', $this->wp_user_id );

			return $user->display_name;
		}

		return $this->get_meta( 'name' );
	}

	/**
	 * Return the email of the chatbot user
	 *
	 * @return mixed|string|null
	 * @since 1.0.0
	 */
	public function email() {
		if ( $this->is_wp_user() ) {
			$user = get_user_by( 'id', $this->wp_user_id );

			return $user->user_email;
		}

		return $this->get_meta( 'email' );
	}

	public function get_ip(): ?string {
		return $this->ip;
	}

	public function set_ip( ?string $ip ): void {
		$this->ip = $ip;
	}

	public function agent() {
		$user = get_user_by( 'ID', $this->get_wp_user_id() );
		if ( ! $user ) {
			return null;
		}
		$controller = new WP_REST_Users_Controller();
		$request    = new WP_REST_Request( 'GET' );
		$response   = $controller->prepare_item_for_response( $user, $request );

		return $controller->prepare_response_for_collection( $response );
	}

	/**
	 * Get the WordPress user ID.
	 *
	 * @return int|null WordPress user ID or null if none assigned.
	 * @since 1.0.0
	 */
	public function get_wp_user_id(): ?int {
		return $this->wp_user_id;
	}

	/**
	 * Set the WordPress user ID.
	 *
	 * @param  int|null  $wp_user_id  WordPress user ID or null.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_wp_user_id( ?int $wp_user_id ): void {
		$this->wp_user_id = $wp_user_id;
	}

	/**
	 * Get the human-readable label for the user type.
	 *
	 * @return string|null The type label or null if type is not set.
	 * @since 1.0.0
	 */
	public function get_type_label(): ?string {
		if ( $this->type === null ) {
			return null;
		}

		switch ( $this->type ) {
			case self::TYPE_GUEST:
				return __( 'Guest', 'limb-chatbot' );
			case self::TYPE_WP_USER:
				return __( 'WordPress User', 'limb-chatbot' );
			case self::TYPE_AGENT:
				return __( 'Agent', 'limb-chatbot' );
			default:
				return __( 'Unknown', 'limb-chatbot' );
		}
	}

	/**
	 * Get country from geolocation or meta.
	 *
	 * @return string|null Country code or name.
	 * @since 1.0.0
	 */
	public function country(): ?string {
		$geolocation = $this->geolocation();

		return $geolocation['country'] ?? null;
	}

	/**
	 * Get geolocation data (country, state, city) for the user.
	 *
	 * First checks meta for cached geolocation data. If not found, fetches from IP
	 * using external geolocation API and stores complete JSON in meta.
	 *
	 * @return array|null Array with 'country', 'state', 'city' keys, or null if unavailable.
	 * @since 1.0.0
	 */
	public function geolocation(): ?array {
		// Check if geolocation is already cached in meta (stored as JSON)
		$cached_geolocation = $this->get_meta( 'geolocation' );
		if ( ! empty( $cached_geolocation ) ) {
			// Try to decode JSON if it's a string
			if ( is_string( $cached_geolocation ) ) {
				$decoded = json_decode( $cached_geolocation, true );
				if ( is_array( $decoded ) ) {
					return $decoded;
				}
			} elseif ( is_array( $cached_geolocation ) ) {
				// Already an array, return it
				return $cached_geolocation;
			}
		}

		// If no IP address, cannot geolocate
		if ( empty( $this->ip ) || '127.0.0.1' == $this->ip ) {
			return null;
		}

		// Fetch geolocation using external API
		$geolocation_data = $this->fetch_geolocation_from_ip( $this->ip );

		if ( empty( $geolocation_data ) ) {
			return null;
		}

		// Extract country, state, city
		$result = array(
			'country' => $geolocation_data['country'] ?? null,
			'state'   => $geolocation_data['state'] ?? null,
			'city'    => $geolocation_data['city'] ?? null,
		);

		// Store complete JSON in meta for future use
		$this->update_meta( 'geolocation', wp_json_encode( $geolocation_data ) );

		return $result;
	}

	/**
	 * Fetch geolocation data from IP address using external API.
	 *
	 * Tries multiple geolocation services as fallback.
	 *
	 * @param  string  $ip_address  IP address to geolocate.
	 *
	 * @return array|null Geolocation data array or null on failure.
	 * @since 1.0.0
	 */
	private function fetch_geolocation_from_ip( string $ip_address ): ?array {
		// Skip local/private IPs
		if ( ! filter_var( $ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
			return null;
		}

		// Try ip-api.com first (free, no API key required, 45 requests/minute)
		$geolocation = $this->fetch_from_ip_api( $ip_address );
		if ( ! empty( $geolocation ) ) {
			return $geolocation;
		}

		// Fallback to ipinfo.io (free tier: 50,000 requests/month)
		$geolocation = $this->fetch_from_ipinfo( $ip_address );
		if ( ! empty( $geolocation ) ) {
			return $geolocation;
		}

		return null;
	}

	/**
	 * Fetch geolocation from ip-api.com.
	 *
	 * @param  string  $ip_address  IP address.
	 *
	 * @return array|null Geolocation data.
	 * @since 1.0.0
	 */
	private function fetch_from_ip_api( string $ip_address ): ?array {
		$url = sprintf( 'http://ip-api.com/json/%s?fields=status,message,country,regionName,city',
			urlencode( $ip_address ) );

		$response = wp_remote_get( $url, array(
			'timeout'     => 5,
			'httpversion' => '1.1',
		) );

		if ( is_wp_error( $response ) ) {
			return null;
		}

		$body = wp_remote_retrieve_body( $response );
		$data = json_decode( $body, true );

		if ( empty( $data ) || ( isset( $data['status'] ) && $data['status'] !== 'success' ) ) {
			return null;
		}

		return array(
			'country' => $data['country'] ?? null,
			'state'   => $data['regionName'] ?? null,
			'city'    => $data['city'] ?? null,
			'source'  => 'ip-api.com',
		);
	}

	/**
	 * Fetch geolocation from ipinfo.io.
	 *
	 * @param  string  $ip_address  IP address.
	 *
	 * @return array|null Geolocation data.
	 * @since 1.0.0
	 */
	private function fetch_from_ipinfo( string $ip_address ): ?array {
		$url = sprintf( 'https://ipinfo.io/%s/json', urlencode( $ip_address ) );

		$response = wp_remote_get( $url, array(
			'timeout'     => 5,
			'httpversion' => '1.1',
		) );

		if ( is_wp_error( $response ) ) {
			return null;
		}

		$body = wp_remote_retrieve_body( $response );
		$data = json_decode( $body, true );

		if ( empty( $data ) || isset( $data['error'] ) ) {
			return null;
		}

		// Parse region (format: "City, State, Country" or "State, Country")
		$region  = $data['region'] ?? null;
		$city    = $data['city'] ?? null;
		$country = $data['country'] ?? null;

		return array(
			'country' => $country,
			'state'   => $region,
			'city'    => $city,
			'source'  => 'ipinfo.io',
		);
	}

	/**
	 * Get wp user object
	 *
	 * @return array|mixed|WP_REST_Response|null
	 */
	public function wp_user() {
		if ( $this->is_wp_user() ) {
			$user = get_user_by( 'ID', $this->get_wp_user_id() );
			if ( ! $user ) {
				return null;
			}
			$controller = new WP_REST_Users_Controller();
			$request    = new WP_REST_Request( 'GET' );
			$response   = $controller->prepare_item_for_response( $user, $request );

			return $controller->prepare_response_for_collection( $response );
		}

		return null;
	}

	/**
	 * Get device information from meta.
	 *
	 * Returns device information stored as JSON in meta.
	 *
	 * @return array|null Device information array or null if not available.
	 * @since 1.0.0
	 */
	public function device_info(): ?array {
		$device_info = $this->get_meta( 'device_info' );
		
		if ( empty( $device_info ) ) {
			return null;
		}

		// If it's a JSON string, decode it
		if ( is_string( $device_info ) ) {
			$decoded = json_decode( $device_info, true );
			if ( is_array( $decoded ) ) {
				return $decoded;
			}
		}

		// If it's already an array, return it
		if ( is_array( $device_info ) ) {
			return $device_info;
		}

		return null;
	}

}