<?php

namespace Mtc\Modules\Members\Models;

use App\Casts\OptionalEncrypted;
use App\Src\Encryption;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Validator;
use Mtc\Core\Admin\User;
use Mtc\Modules\Members\Classes\Auth;
use Mtc\Modules\Members\Classes\MemberDuplicateCheck;
use Mtc\Modules\Members\Classes\MemberOauthProviders;
use Mtc\Modules\Members\Classes\MembersBasket;
use Mtc\Plugins\Clinic\Src\Logger;
use Mtc\Plugins\NHS\Classes\NHSMember;
use Mtc\Shop\Assessment\Assessment;
use Mtc\Shop\Basket;
use Mtc\Shop\Events\MemberNoteCreatedEvent;
use Mtc\Shop\Order;

/**
 * Eloquent model Member.
 *
 * @author Aleksey Lavrinenko
 * @version 23.08.2016.
 *
 * Database fields:
 * @property int $id
 * @property string $email
 * @property string $contact_no
 * @property string $type
 * @property string $hash
 * @property int $height
 *
 * accessors
 * @property string $fullname
 *
 * @property Carbon $created_at
 * @property Carbon $updated_at
 * @property Collection $addresses
 * @property MembersAddress $addressBilling
 */
class Member extends Model
{
    use MembersBasket;

    protected $table = 'members';

    protected $fillable = [
        'unio_internal_id',
        'is_external',
        'is_activated',
        'email',
        'phone_prefix',
        'contact_no',
        'type',
        'verification_code',
        'verification_code_expiry',
        'prescription_reminder_email',
        'prescription_reminder_sms',
        'dob',
        'gender',
        'height',
        'first_login',
        'referrer_member_id',
    ];

    protected $casts = [
        'prescription_reminder_email' => 'bool',
        'prescription_reminder_sms' => 'bool',
        'email' => OptionalEncrypted::class,
        'contact_no' => OptionalEncrypted::class,
        'conditions' => OptionalEncrypted::class,
        'medications' => OptionalEncrypted::class,
        'allergies' => OptionalEncrypted::class,
        'dob' => OptionalEncrypted::class,
    ];

    public static $searchable = [
        'email',
        'contact_no',
        'dob',
    ];

    private $rules = [
        'confirmed_terms_at' => 'required | date',
        'has_nominated_client_as_pharmacy_of_choice' => 'required',
    ];

    protected static function boot() : void
    {
        parent::boot();

        self::saved(function (self $model) {
            Event::dispatch('Member@saved', $model);
            Encryption::updateHashes($model);
        });
    }

    /**
     * Get DOB in DD.MM.YYYY format
     *
     * @return string
     */
    public function getDob(): string
    {
        if (empty($this->dob)) {
            return '';
        }
        try {
            return Carbon::parse($this->dob)->format('d.m.Y');
        } catch (Exception) {
            return '';
        }
    }


    public function getAge() : int
    {
        try {
            return Carbon::parse($this->dob)->age;
        } catch(Exception $e) {
            return 0;
        }
    }

    private $validation_messages = [];

    public function validate()
    {
        return Validator::make($this->attributes, $this->rules, $this->validation_messages);
    }


    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password'
    ];

    public static $titles = [
        'Mr.',
        'Mrs.',
        'Ms.',
    ];

    public static $genders = [
        'Male',
        'Female',
    ];

    protected $appends = [
        'full_name',
    ];

    /**
     * fullname accessor. Returns Member's full name
     *
     * @return string
     */
    public function getFullnameAttribute()
    {
        $full_name = 'Customer';

        if ($this->addressBilling) {
            $full_name = $this->addressBilling->firstname . ' ' . $this->addressBilling->lastname;
        }

        return $full_name;
    }

    /**
     * password mutator. Hashes the password before setting.
     * @param $password
     */
    public function setPassword($password)
    {
        $this->password = Auth::hashPassword($password);
    }


    // SCOPES

    /**
     * scope withEmail($email). filter members by email
     *
     * @param $query
     * @param $email
     * @return mixed
     */
    public function scopeWithEmail($query, $email)
    {
        return $query->where('email', '=', $email);
    }

    // RELATIONSHIPS

    /**
     * addresses one-to-many relationship.
     *
     * @return HasMany
     */
    public function addresses()
    {
        return $this->hasMany(MembersAddress::class, 'member_id');
    }

    /**
     * addressBilling one-to-one relationship.
     *
     * @return HasOne
     */
    public function addressBilling()
    {
        return $this->hasOne(MembersAddress::class, 'member_id')->billing();
    }

    /**
     * addressShipping one-to-one relationship.
     *
     * @return HasOne
     */
    public function addressShipping()
    {
        return $this->hasOne(MembersAddress::class, 'member_id')->shipping();
    }

    /**
     * Relationship with 3rd party authenticators for this member
     *
     * @return HasMany
     */
    public function authenticators()
    {
        return $this->hasMany(MemberOauthProviders::class, 'member_id');
    }

    public function patientNotes(): HasMany
    {
        return $this->hasMany(MemberNote::class, 'member_id');
    }

    /**
     * Relationship to NHS Member
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasOne
     */
    public function nhs_member()
    {
        return $this->hasOne(NHSMember::class);
    }

    public function create_nhs_member()
    {
        $this->refresh();

        if (! $this->nhs_member) {

            if (! $this->addressBilling) {
                throw new Exception("Billing Address is required to create an NHS Member.");
            }

            $nhs_member = new NHSMember();
            $nhs_member->firstname = $this->addressBilling->firstname;
            $nhs_member->lastname = $this->addressBilling->lastname;
            $nhs_member->is_patient = 1;
            $nhs_member->dob = $this->dob;
            $nhs_member->member()->associate($this);
            $nhs_member->save();
        }

        return $this->refresh();
    }


    public function referrer()
    {
        return $this->belongsTo(Member::class, 'referrer_member_id');
    }


    public function referred_members()
    {
        return $this->hasMany(Member::class, 'referrer_member_id');
    }


    /**
     * Relationship to Order(s)
     *
     * @return HasMany
     */
    public function orders()
    {
        return $this->hasMany(Order::class, 'member');
    }

    /**
     * Relationship to attributes
     *
     * @return HasMany
     */
    public function attributes(): HasMany
    {
        return $this->hasMany(MemberAttribute::class, 'member_id');
    }

    /**
     * Function to check if current url is within members area
     *
     * @return boolean
     *
     * @author Vladislavs Ignatjevs
     */
    public static function inMembersUrl()
    {
        return (stripos($_SERVER['REQUEST_URI'], "/members") !== false);
    }

    /**
     * Tries to locate duplicate member by combinations of user data
     *
     * @param Member $member
     * @return Model|null|static
     */
    public static function getDuplicate(Member $member)
    {
        // First locate by phone number + address
        $parent_member = (new self())
            ->orderBy('id', 'desc')
            ->join('members_addresses', function($join) use ($member) {
                $join->on('members_addresses.member_id', '=', 'members.id')
                    ->where('members_addresses.type', 'shipping')
                    ->where('members_addresses.address1', 'LIKE', $member->addressBilling->address1);
            })
            ->where('members.contact_no', '', $member->contact_no)
            ->where('members.id', '<>', $member->id)
            ->first();

        // If not found then try locating by full address + firstname combination
        if (empty($parent_member)) {
            $parent_member = (new self())
                ->orderBy('id', 'desc')
                ->join('members_addresses', function ($join) use ($member) {
                    $join->on('members_addresses.member_id', '=', 'members.id')
                        ->where('members_addresses.type', 'shipping')
                        ->where('members_addresses.address1', 'LIKE', $member->addressBilling->address1)
                        ->where('members_addresses.address2', 'LIKE', $member->addressBilling->address2)
                        ->where('members_addresses.city', 'LIKE', $member->addressBilling->city)
                        ->where('members_addresses.postcode', 'LIKE', $member->addressBilling->postcode)
                        ->where('members_addresses.firstname', 'LIKE', $member->addressBilling->firstname);
                })
                ->where('members.id', '<>', $member->id)
                ->first();
        }
        return $parent_member;
    }

    /**
     * Determines whether a duplicate member exists in the system based on user input.
     *
     * Accepts a flat array of identifying fields (email, phone, dob, postcode,
     * firstname, lastname) plus an optional exclude_member_id to skip when checking
     * an existing member's own record. Returns the field to amend, or false.
     *
     * @param array $data  Keys: email, phone, dob, postcode, firstname, lastname, exclude_member_id
     * @return string|false
     */
    public static function memberHasDuplicates(array $data): string|false
    {
        return (App::make(MemberDuplicateCheck::class))->hasDuplicates($data);
    }

    /**
     * Get member stats
     *
     * @param $member_id
     * @return array
     */
    public static function getMemberStats($member_id) {
        if (empty($member_id)) {
            return [];
        }
        $stats = [
            'average_order_value' => 0,
            'lifetime_value' => 0,
            'number_of_orders' => 0,
            'last_login' => '-',
        ];
        $stats['number_of_orders'] = $order_count = Order::query()->where('member', $member_id)->where('paid', '1')->count();
        if ($order_count > 0) {
            $stats['lifetime_value'] = Order::query()->where('member', $member_id)->sum('cost_total');
            $stats['average_order_value'] = number_format($stats['lifetime_value'] / $order_count, 2, '.', '');
        }
        $last_login = (new Member())->find($member_id)->last_login;
        if ($last_login !== '0000-00-00 00:00:00') {
            $stats['last_login'] = date_convert_db_to_dmy($last_login);
            $stats['last_login'] = convert_date($last_login, 'd/m/Y H:i:s');
        }
        return $stats;
    }

    /**
     * @param $basket_id
     */
    public function setBasketDetails($basket_id)
    {
        /** @var Basket $basket */
        $basket = Basket::query()
            ->with(['billingAddress', 'shippingAddress', 'info'])
            ->find($basket_id);

        $basket->member = $this->id;
        $basket->save();

        $data = [
            'type' => 'billing',
            'firstname' => $this->addressBilling->firstname,
            'lastname' => $this->addressBilling->lastname,
            'address1' => $this->addressBilling->address1,
            'address2' => $this->addressBilling->address2,
            'city' => $this->addressBilling->city,
            'postcode' => $this->addressBilling->postcode,
            'country' => $this->addressBilling->country,
            'state' => $this->addressBilling->state,
            'title' => $this->addressBilling->title,
            'gender' => $this->gender,
        ];

        if ($basket->billingAddress == null) {
            $basket->billingAddress()->create($data);
        } elseif (empty($basket->billingAddress->address1)) {
            $basket->billingAddress->fill($data);
            $basket->billingAddress->save();
        }

        $info = [
            'email' => $this->email,
            'contact_no' => $this->contact_no,
            'phone_prefix' => $this->phone_prefix,
            'dob' => $this->dob ?? '',
        ];

        if ($basket->info == null) {
            $basket->info()->create($info);
        } else {
            $basket->info->fill($info);
            $basket->info->save();
        }

        // Reset session and reload basket as data has now been changed
        $new_basket = new \Basket();
        $new_basket->resetSession();
        $new_basket->Go_Basket();

    }

    public function setAllergies(array $items)
    {
        $this->allergies = json_encode($items);
    }

    public function setConditions(array $items)
    {
        $this->conditions = json_encode($items);
    }

    public function setMedications(array $items)
    {
        $this->medications = json_encode($items);
    }



    public function getAllergiesString($glue = ', ')
    {
        return implode($glue, $this->getAllergiesArray());
    }

    public function getConditionsString($glue = ', ')
    {
        return implode($glue, $this->getConditionsArray());
    }

    public function getMedicationsString($glue = ', ')
    {
        return implode($glue, $this->getMedicationsArray());
    }



    public function getAllergiesArray()
    {
        return (array)json_decode($this->allergies, true);
    }

    public function getConditionsArray()
    {
        return (array)json_decode($this->conditions, true);
    }

    public function getMedicationsArray()
    {
        return (array)json_decode($this->medications, true);
    }


    private function prepare_data_from_input(array &$data)
    {
        if (! trim($data['email'])) {
            $data['email'] = null;
        }

        if (! trim($data['contact_no'])) {
            $data['contact_no'] = null;
        }
    }


    public function update_from_input(array $data)
    {
        // Table 'members'.

        $this->prepare_data_from_input($data);
        $this->fill($data)->save();


        // Table 'members_addresses'.

        $addresses = (array)Arr::get($data, 'addresses');
        foreach ($addresses as $address) {
            switch ($address['type']) {
                case 'shipping':
                    if ($this->addressShipping) {
                        $this->addressShipping->fill($address)->save();
                    } else {
                        $this->addressShipping()->create($address);
                    }
                    break;

                case 'billing':
                    if ($this->addressBilling) {
                        $this->addressBilling->fill($address)->save();
                    } else {
                        $this->addressBilling()->create($address);
                    }
                    break;
            }
        }


        // Table 'nhs_members'.

        $this->create_nhs_member();
        if ($nhs_member_data = Arr::get($data, 'nhs_member')) {
            $this->nhs_member->fill($nhs_member_data);
            $this->nhs_member->save();
        }

        if ($doctor_surgery_data = Arr::get($data, 'doctor_surgery')) {
            if ($this->nhs_member->doctor_surgery) {
                $this->nhs_member->doctor_surgery->fill($doctor_surgery_data)->save();
            } else {
                $this->nhs_member->doctor_surgery()->create($doctor_surgery_data);
            }
        }

    }


    public function can_manage_own_account()
    {
        $can_manage_own_account = false;

        if (! $this->referrer_member_id) {
            $can_manage_own_account = true;
        }

        return $can_manage_own_account;
    }


    public function is_allowed_to_log_in_as_member(Member $member)
    {
        $is_allowed = false;

        if ($this->id) {

            if ($member->referrer_member_id == $this->id) {
                $is_allowed = true;
            }

            if (
                $this->referrer_member_id == $member->id &&
                $_SESSION['IS_ALLOWED_TO_LOG_IN_AS_REFERRER']
            ) {
                $is_allowed = true;
            }

        }

        return $is_allowed;
    }


    public function try_to_log_in()
    {
        $currently_logged_in_member = Auth::getLoggedInMember();

        if (! $currently_logged_in_member->is_allowed_to_log_in_as_member($this)) {
            abort(403, 'You have no permissions to do this.');
            exit;
        }

        Auth::login($this);
        if ($this->can_manage_own_account()) {
            unset($_SESSION['IS_ALLOWED_TO_LOG_IN_AS_REFERRER']);
        } else {
            $_SESSION['IS_ALLOWED_TO_LOG_IN_AS_REFERRER'] = true;
        }
    }


    public function is_email_system_generated()
    {
        $is_system_generated = false;

        if (preg_match("/^patient@.+\.mtc$/", $this->email)) {
            $is_system_generated = true;
        }

        return $is_system_generated;
    }


    public function deactivate()
    {
        $this->is_activated = false;
        $this->save();
    }


    public function getAdminLink()
    {
        return SITE_URL . "/modules/Members/admin/manage_member.php?id={$this->id}";
    }

    /**
     * After logging in / registering, link the completed assessments with the member
     *
     * @return void
     */
    public function linkAssessmentsFromSession(): void
    {
        $assessmentIds = [];
        if (!empty($_SESSION['assessments'])) {
            $assessmentIds = array_values($_SESSION['assessments']);
        }
        if (!empty($_SESSION['category_assessments'])) {
            $categoryAssessmentIds = array_values($_SESSION['category_assessments']);
            $assessmentIds = array_merge($categoryAssessmentIds, $assessmentIds);
        }

        if (!empty($assessmentIds)) {
            Assessment::query()
                ->whereIn('id', $assessmentIds)
                ->where('member_id', 0)
                ->update([
                    'member_id' => $this->id,
                ]);
        }

        // Clear stale session references after linking
        unset($_SESSION['assessments']);
        unset($_SESSION['category_assessments']);
        unset($_SESSION[Assessment::LATEST_ASSESSMENT_SESSION_KEY]);
    }

    /**
     * Gets DOB options for FE
     *
     * @return array[]
     */
    public static function getDobOptions(): array
    {
        $date_days = [];

        for ($x=1; $x<32; $x++) {

            $day = $x;

            if ($x < 10) {
                $day = '0' . $x;
            }

            $date_days[] = [
                'text'  => $day,
                'value' => $x
            ];
        }

        $date_months = [];

        for ($x=1; $x<13; $x++) {

            $month = $x;

            if ($x < 10) {
                $month = '0' . $x;
            }

            $date_months[] = [
                'text'  => $month,
                'value' => $x
            ];
        }

        $date_years = [];
        $this_year = (int) Date("Y");
        $min_order_age = 16;

        for ($x=($this_year - $min_order_age); $x >= 1910; $x--) {

            $year = $x;

            if ($x < 10) {
                $year = '0' . $x;
            }

            $date_years[] = [
                'text'  => $year,
                'value' => $x
            ];
        }
        return [
            'days' => $date_days,
            'months' => $date_months,
            'years' => $date_years
        ];
    }

    /**
     * Logs patient notes
     *
     * @param array $params
     * @return void
     */
    public function logPatientNotes(array $params): void
    {
        $adminUser = User::getAdminUser();

        $isFlag = !empty($params['isFlag']);

        /** @var MemberNote $memberNote */
        $memberNote = MemberNote::query()
            ->create([
                'member_id' => $this->id,
                'order_id' => $params['orderID'] ?? null,
                'admin_user_id' => $adminUser->id,
                'content' => $params['note'],
                'is_flag' => $isFlag,
                'flag_due' => $params['flagDue'] ?? null,
                'status' => $params['status'] ?? null,
            ]);

        (new Logger($memberNote, $adminUser, $this->id))
            ->log($isFlag ? Logger::MEMBER_FLAG : Logger::MEMBER_NOTE, Logger::ACTION_CREATED, [
                'content' => $params['note'],
            ]);

        Event::dispatch(MemberNoteCreatedEvent::class, new MemberNoteCreatedEvent($memberNote));
    }

    /**
     * Search members by ID, name, email, phone, or postcode.
     *
     * Phase 1: Fast hash-based exact matching (uses indexed _hash columns).
     * Phase 2: Falls back to in-memory partial matching by decrypting in
     *          chunks, stopping after 15 results. Only triggers when the
     *          exact match finds nothing and the query is >= 3 characters.
     *
     * @param string $searchQuery
     * @return array
     */
    public static function searchMembers(string $searchQuery): array
    {
        if (empty($searchQuery)) {
            return [];
        }

        $searchQuery = trim($searchQuery);
        $hash = Encryption::makeHash($searchQuery);
        $parts = preg_split('/\s+/', $searchQuery);

        // Phase 1: Fast hash-based exact matching
        $members = Member::query()
            ->with(['addressBilling', 'addressShipping'])
            ->whereHas('addressBilling')
            ->where(function (Builder $query) use ($searchQuery, $hash, $parts) {
                if (is_numeric($searchQuery)) {
                    $query->orWhere('members.id', $searchQuery);
                }

                $query->orWhere('email_hash', $hash)
                    ->orWhere('contact_no_hash', $hash);

                $query->orWhereHas('addressBilling', function (Builder $q) use ($hash) {
                    $q->where('firstname_hash', $hash)
                        ->orWhere('lastname_hash', $hash)
                        ->orWhere('postcode_hash', $hash);
                });

                if (count($parts) === 2) {
                    $firstHash = Encryption::makeHash($parts[0]);
                    $lastHash = Encryption::makeHash($parts[1]);

                    $query->orWhereHas('addressBilling', function (Builder $q) use ($firstHash, $lastHash) {
                        $q->where('firstname_hash', $firstHash)
                            ->where('lastname_hash', $lastHash);
                    });
                }
            })
            ->limit(15)
            ->get();

        // Phase 2: In-memory partial matching fallback
        if ($members->isEmpty() && strlen($searchQuery) >= 3) {
            $lowerQuery = strtolower($searchQuery);
            $results = collect();

            Member::query()
                ->with(['addressBilling', 'addressShipping'])
                ->whereHas('addressBilling')
                ->chunkById(100, function ($chunk) use ($lowerQuery, &$results) {
                    foreach ($chunk as $member) {
                        $address = $member->addressBilling;
                        if (!$address) {
                            continue;
                        }

                        $fields = [
                            $address->firstname,
                            $address->lastname,
                            $member->email,
                            $member->contact_no,
                            $address->postcode,
                            $address->address1,
                            $address->address2,
                            $address->city,
                        ];

                        foreach ($fields as $field) {
                            if ($field && str_contains(strtolower($field), $lowerQuery)) {
                                $results->push($member);
                                break;
                            }
                        }

                        if ($results->count() >= 15) {
                            return false;
                        }
                    }
                });

            $members = $results;
        }

        return $members->map(function (Member $member) {
            $billing = $member->addressBilling;
            $shipping = $member->addressShipping;

            return [
                'id' => $member->id,
                'firstname' => $billing->firstname ?? null,
                'lastname' => $billing->lastname ?? null,
                'email' => $member->email,
                'contact_no' => $member->contact_no,
                'address1' => $billing->address1 ?? null,
                'address2' => $billing->address2 ?? null,
                'city' => $billing->city ?? null,
                'postcode' => $billing->postcode ?? null,
                'address_shipping' => $shipping ? [
                    'firstname' => $shipping->firstname,
                    'lastname' => $shipping->lastname,
                    'address1' => $shipping->address1,
                    'address2' => $shipping->address2,
                    'city' => $shipping->city,
                    'postcode' => $shipping->postcode,
                    'country' => $shipping->country ?? 'GB',
                ] : null,
            ];
        })->toArray();
    }

    /**
     * Whether member's password is empty
     *
     * @return bool
     */
    public function emptyPassword(): bool
    {
        if (empty($this->id)) {
            return true;
        }
        $this->makeVisible(['password']);
        $emptyPassword = empty($this->password);
        $this->makeHidden(['password']);
        return $emptyPassword;
    }

    /**
     * Whether the member requires account verification
     *
     * @return bool
     */
    public function requiresAccountVerification(): bool
    {
        if (empty($this->id)) {
            return false;
        }
        if (!empty($_SESSION['adminId'])) {
            return false;
        }
        return empty($this->account_verified);
    }

    public function subscriptions()
    {
        return $this->hasMany(Subscription::class, 'member_id');
    }
}
