<?php
/**
 * User Model Abstract for managing all wp_users and wp_usermeta
 *
 * @package Booktics/Abstracts
 */

namespace Booktics\Abstracts;

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

use JsonSerializable;
use WP_User_Query;

abstract class User_Model implements JsonSerializable {

    /**
     * Store specific user role
     *
     * @var string
     */
    protected $role = 'administrator';

    /**
     * Store specific meta prefix
     *
     * @var string
     */
    protected $meta_prefix = '';

    /**
     * Store user id
     *
     * @var integer
     */
    protected $id;

    /**
     * Store user data
     *
     * @var array
     */
    protected $fillable = [
        'id'              => '',
        'first_name'      => '',
        'last_name'       => '',
        'user_email'      => '',
        'image'           => '',
        'user_login'      => '',
        'phone'           => '',
        'status'          => 0,
        'user_registered' => '',
    ];

    /**
     * Store Filter conditions
     *
     * @var array
     */
    protected $filters = [];

    /**
     * Store error
     *
     * @var \WP_Error
     */
    protected $error;

    /**
     * User constructor
     *
     * @param $user
     *
     * return void
     */
    public function __construct( $user = 0 ) {
        require_once( ABSPATH . 'wp-admin/includes/user.php' );

        if ( $user instanceof self ) {
            $this->set_id( $user->get_id() );
        } elseif ( ! empty( $user->ID ) ) {
            $this->set_id( $user->ID );
        } elseif ( is_numeric( $user ) && $user > 0 ) {
            $this->set_id( $user );
        }
    }

    /**
     * Get user id
     *
     * @return integer
     */
    public function get_id(): int {
        return $this->id;
    }

    /**
     * Set user id
     *
     * @param int $id
     *
     * @return void
     */
    public function set_id( int $id ) {
        $this->id = $id;
    }

    /**
     * Get user data
     *
     * @param string $name
     *
     * @return string|null
     */
    public function __get( string $name ) {
        if ( $name === 'id' ) {
            return $this->id;
        }
        if ( ! isset( $this->fillable[ $name ] ) ) {
            return null;
        }

        $possible_function_name = 'get_' . booktics_snake_case( $name ) . '_attribute';
        $data                   = $this->get_prop( $name, $this->fillable[ $name ] );
        if ( method_exists( $this, $possible_function_name ) ) {
            return $this->$possible_function_name( $this );
        }

        return $data;
    }

    /**
     * Get either user data or default data
     *
     * @param string $prop
     * @param string|array $default
     *
     * @return string|array
     */
    private function get_prop( $prop = '', $default = '' ) {
        return $this->get_metadata( $prop ) ?? $default;
    }

    /**
     * Get user metadata
     *
     * @param string $prop
     *
     * @return mixed
     */
    private function get_metadata( string $prop = '' ) {
        $meta_key = $this->meta_prefix . $prop;

        return get_user_meta( $this->id, $meta_key, true );
    }

    /**
     * Set user values
     *
     * @param string $name
     * @param string $value
     *
     * @return void
     */
    public function __set( string $name, string $value ) {
        if ( strpos( $name, $this->meta_prefix ) !== false ) {
            $name = str_replace( $this->meta_prefix, '', $name );
        }

        $this->fillable[ $name ] = $value;
    }

    /**
     * Save or update user
     *
     * @param array $args
     *
     * @return mixed|string
     */
    public function save( array $args = [] ) {
        if ( isset( $args['email'] ) ) {
            $args['user_email'] = $args['email'];
            unset( $args['email'] );
        }

        $user_id = $args['ID'] ?? '';

        $user_data = [
            'ID'       => $user_id,
            'role'     => $this->role,
            'password' => wp_generate_password(),
        ];

        // Add required user fields
        foreach ( $args as $key => $value ) {
            if ( ! isset( $this->fillable[ $key ] ) && ! in_array( $key, [
                    'email',
                    'user_email'
                ] ) ) {
                continue;
            }
            $user_data[ $key ] = $value;
        }

        $user_id = wp_insert_user( $user_data );
        if ( is_wp_error( $user_id ) ) {
            return $user_id;
        }

        $user = new \WP_User( $user_id );
        $user->add_role( $this->role );
        $this->save_metadata( $args );

        if ( $user_id ) {
            wp_update_user( $user_data );
        } else {
            $user_id = wp_insert_user( $user_data );
        }

        $user = get_userdata( $user_id );
        $user->add_role( $this->role );
        if ( ! is_wp_error( $user_id ) ) {
            $this->set_id( $user_id );
            $this->save_metadata( $user_data );
            $this->retrieve_password();
        }

        return $user_id;
    }

    /**
     * Update specific field in user or meta data
     *
     * @param array $data
     *
     * @return bool
     */
    public function update( array $data = [] ): bool {
        if ( ! $this->id ) {
            $this->error = new \WP_Error( 'no_id', __( 'User ID is required to update.', 'booktics' ) );

            return false;
        }

        $user_data = [ 'ID' => $this->id ];
        $meta_data = [];

        foreach ( $data as $key => $value ) {
            if ( in_array( $key, [ 'schedule', 'custom', 'holiday' ] ) ) {
                continue;
            }
            if ( isset( $this->fillable[ $key ] ) && ! strpos( $key, $this->meta_prefix ) ) {
                if ( in_array( $key, [ 'user_email', 'user_login' ] ) ) {
                    $user_data[ $key ] = $value;
                }
                $meta_data[ $key ] = $value;
            }
        }
        $result = wp_update_user( $user_data );
        if ( is_wp_error( $result ) ) {
            $this->error = $result;

            return false;
        }

        $this->save_metadata( $meta_data );

        return true;
    }

    /**
     * Store user meta data
     *
     * @param array $data
     *
     * @return void
     */
    public function save_metadata( array $data = [] ) {
        foreach ( $data as $key => $value ) {
            if ( ! isset( $this->fillable[ $key ] ) ) {
                continue;
            }
            $meta_key = $this->meta_prefix . $key;

            update_user_meta( $this->id, $meta_key, $value );
        }
    }

    /**
     * Send password reset email
     *
     * @return void
     */
    public function retrieve_password() {
        if ( 0 === intval( $this->status ) ) {
            retrieve_password( $this->email );
        }
    }

    /**
     * Delete user
     *
     * @return boolean
     */
    public function delete(): bool {
        $user = get_userdata( $this->id );
 
        if ( ! $user ) {
            return false;
        }

        if ( count( $user->roles ) == 1 && in_array( $this->role, $user->roles ) ) { 
            return wp_delete_user( $this->id );
        }
        $user->remove_role( $this->role );

        return true;
    }

    /**
     * Append user filter condition
     *
     * @param string $field
     * @param $value
     * @param string $compare
     *
     * @return $this
     */
    public function where( string $field, $value, string $compare = '=' ): self {
        $this->filters[] = [
            'field'   => $field,
            'value'   => $value,
            'compare' => $compare,
        ];

        return $this;
    }

    /**
     * Add OR conditions in where
     *
     * @param string $field
     * @param $value
     * @param string $compare
     *
     * @return $this
     */
    public function or_where( string $field, $value, string $compare = '=' ): self {
        $this->filters[] = [
            'field'    => $field,
            'value'    => $value,
            'compare'  => $compare,
            'relation' => 'OR',
        ];

        return $this;
    }

    /**
     * Conditionally apply query filters
     *
     * @param mixed $value
     * @param callable $callback
     * @param callable|null $default
     *
     * @return $this
     */
    public function when( $value, callable $callback, callable $default = null ): self {
        if ( $value ) {
            $callback( $this, $value );
        } elseif ( $default ) {
            $default( $this, $value );
        }

        return $this;
    }

    /**
     * Get all users paginated data
     *
     * @param array $args
     *
     * @return array
     */
    public function all( array $args = [] ): array {
        $defaults = [
            'role'    => $this->role,
            'number'  => 20,
            'paged'   => 1,
            'orderby' => 'ID',
            'order'   => 'DESC',
        ];

        $args = wp_parse_args( $args, $defaults );

        if ( ! empty( $this->filters ) ) {
            $meta_query = [];
            $relation   = 'AND';

            foreach ( $this->filters as $filter ) {
                if ( in_array( $filter['field'], [
                    'user_email',
                    'user_login'
                ] ) ) {
                    $args['search']         = '*' . esc_attr( $filter['value'] ) . '*';
                    $args['search_columns'] = [ $filter['field'] ];
                } else {
                    $meta_query[] = [
                        'key'     => $this->meta_prefix . $filter['field'],
                        'value'   => $filter['value'],
                        'compare' => $filter['compare'],
                    ];
                }

                if ( isset( $filter['relation'] ) && $filter['relation'] === 'OR' ) {
                    $relation = 'OR';
                }
            }
            if ( ! empty( $meta_query ) ) {
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering users by meta fields
                $args['meta_query'] = [
                    'relation' => $relation,
                    ...$meta_query
                ];
            }
        }

        $users = new WP_User_Query( $args );
        $items = [];
        foreach ( $users->get_results() as $result ) {
            $cloneObj = clone $this;
            $userMeta = get_user_meta( $result->ID );

            $items[] = $cloneObj->hydrate( $result->ID, array_merge(
                (array) $result->data,
                $userMeta
            ) )->to_array();
        }

        $this->filters = [];

        return [
            'total'        => $users->get_total(),
            'per_page'     => $args['number'],
            'current_page' => $args['paged'],
            'last_page'    => ceil( $users->get_total() / $args['number'] ),
            'items'        => $items,
        ];
    }

    /**
     * Find user by id
     *
     * @param $id
     *
     * @return $this
     */
    public function find( $id ) {
        $user = get_user_by( 'ID', $id );

        if ( ! $user ) {
            return null;
        }

        $meta = array_map( function ( $value ) {
            return is_array( $value ) && count( $value ) === 1 ? $value[0] : $value;
        }, get_user_meta( $user->ID ) );

        $user_data = (array) $user->data;

        return $this->hydrate( $id, array_merge( $user_data, $meta ) );
    }

    /**
     * Hydrate each result with this class
     *
     * @param $id
     * @param $result
     *
     * @return $this
     */
    public function hydrate( $id, $result ): self {
        $this->set_id( $id );
        if ( ! is_array( $result ) ) {
            return $this;
        }

        foreach ( $result as $key => $value ) {
            $key = str_replace( $this->meta_prefix, '', $key );
            if ( ! isset( $this->fillable[ $key ] ) && ! in_array( $key, [
                    'user_email',
                    'email'
                ] ) ) {
                continue;
            }
            if ( is_array( $value ) ) {
                $value = implode( ',', $value );
            }

            if ( $key === 'user_registered' ) {
                $this->fillable['user_registered'] = ! empty( $result['user_registered'] ) ?
                    gmdate( 'j F, Y', strtotime( $result['user_registered'] ) ) : '';
                continue;
            }

            $this->$key = empty( $value ) ? $this->fillable[ $key ] : $value;
        }

        return $this;
    }

    /**
     * Generate username from email
     *
     * @param string $email
     *
     * @return  string
     */
    public static function generate_username( string $email ) {
        $username = strtok( $email, '@' );

        if ( username_exists( $username ) ) {
            $username = $username . wp_rand( 10, 100 );
        }

        return $username;
    }

    /**
     * Convert User model object to array
     *
     * @return array
     */
    public function to_array(): array {
        $data = [];

        foreach ( $this->fillable as $key => $default ) {
            if ( $key === 'user_email' ) {
                $data['email'] = $this->fillable[$key];
                continue;
            }

            $value = $this->$key ?? $this->fillable[ $key ];

            // Unserialize if necessary
            if ( is_string( $value ) && $this->is_serialized( $value ) ) {
                $value = maybe_unserialize( $value );
            }

            // Normalize value to default type
            if ( gettype( $value ) !== gettype( $default ) ) {
                if ( gettype( $default ) === 'array' && is_string( $value ) && $this->is_serialized( $value ) ) {
                    $value = maybe_unserialize( $value );
                } elseif ( gettype( $default ) === 'array' ) {
                    $value = $default;
                }
            }

            // Cast specific keys to objects
            if ( 'schedule' == $key ) {
                $value = (object) $value;
            }

            // Cast specific keys to boolean
            if ( in_array( $key, [
                'enable_custom_schedule',
                'enable_custom_holiday',
                'enable_custom_availability',
            ] ) ) {
                $value = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
            }

            if ( 'user_registered' == $key ) {
                $value = $default;
            }

            $data[ $key ] = $value ?? $default;
        }

        $data['ID'] = $this->id;

        return $data;
    }

    /**
     * Check if a string is serialized data
     *
     * @param mixed $data Data to check for serialization
     *
     * @return bool True if data is serialized, false otherwise
     */
    function is_serialized( $data ) {
        return is_string( $data ) && @unserialize( $data ) !== false;
    }

    /**
     * Specify data which should be serialized to JSON.
     *
     * @return array
     */
    public function jsonSerialize(): array {
        return $this->to_array();
    }

    /**
     * Get the first user matching the current filters.
     *
     * @param array $args
     * @return self|null
     */
    public function first(array $args = []): ?self {
        $args['number'] = 1;
        $args['paged'] = 1;
        $result = $this->all($args);
        if (!empty($result['items'][0])) {
            return $this->hydrate($result['items'][0]['ID'], $result['items'][0]);
        }
        return null;
    }
}