<?php

namespace Mtc\Modules\Members\Classes;

use App\Events\MemberCreatedEvent;
use App\Events\MemberUpdatedEvent;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Event;
use App\Src\Encryption;
use Mtc\Modules\Members\Classes\Events\SavedMember;
use Mtc\Modules\Members\Classes\Events\ValidatingMember;
use Mtc\Modules\Members\Models\Member;
use Mtc\Modules\Members\Models\MembersAddress;
use Mtc\Plugins\SMS\Classes\SMS;

/**
 * MemberManager class represents/handles authenticated and authorised users of the
 * system
 *
 * @author Lukas Giegerich <lukas.giegerich@mtcmedia.co.uk>
 * @author Andrew Morgan <andrew.morgan@mtcmedia.co.uk>
 * @author Aleksey Lavrinenko <aleksey.lavrinenko@mtcmedia.co.uk>
 */
class MemberManager
{
    /**
     * Stores user input from the request
     * @var array
     */
    private $user_input;

    /**
     * List of validation errors
     * @var array
     */
    private $errors;

    public Member|null $oldMember = null;

    /**
     * Member::memberExists()
     *
     * @param $email
     * @param null $id
     * @return bool
     */
    public function memberExists($email, $id = null): bool
    {
        $query = Member::query()
            ->where('email_hash', Encryption::makeHash($email));

        if ($id !== null) {
            $query->where('id', $id);
        }

        return $query->exists();
    }

    /**
     * Takes data and inserts or updates members tables with it
     *
     * @param array $args Would typically be POST or GET array
     * @return mixed Members ID on success, false on failure
     */
    public function save(array $args, Member $member, $allow_no_email = false)
    {
        $this->oldMember = clone $member;
        $this->oldMember->load('addresses', 'addressBilling');
        $isNewMember = false;
        $args = collect($args)
            ->map(function ($argument) {
                return strip_tags(filter_var($argument, FILTER_SANITIZE_STRING));
            })->toArray();

        $this->user_input = $args;

        //ensure provided data is valid
        if (!$this->validate($member, $allow_no_email)) {
            return false;
        }

        // T79986 - Capitalize first and last names
        foreach (['billing_firstname', 'billing_lastname'] as $name) {
            $args[$name] = ucwords($args[$name]);
        }
        if (!empty($args['dob_year']) && !empty($args['dob_month']) && !empty($args['dob_date'])) {
            $args['dob'] = $args['dob_year'] . '-' . $args['dob_month'] . '-' . $args['dob_date'];
        } else {
            $args['dob'] = null;
        }

        // prevent patients from changing DOB once set (admins can still change it)
        if (!empty($member->dob) && empty($_SESSION['adminId'])) {
            $args['dob'] = $member->dob;
        }
        $member->fill($args);

        // set medical fields via setters (converts comma-separated string to JSON)
        if (array_key_exists('allergies', $args)) {
            $items = !empty(trim($args['allergies'] ?? ''))
                ? array_filter(array_map('trim', explode(',', $args['allergies'])), 'strlen')
                : [];
            $member->setAllergies($items);
        }
        if (array_key_exists('medications', $args)) {
            $items = !empty(trim($args['medications'] ?? ''))
                ? array_filter(array_map('trim', explode(',', $args['medications'])), 'strlen')
                : [];
            $member->setMedications($items);
        }
        if (array_key_exists('conditions', $args)) {
            $items = !empty(trim($args['conditions'] ?? ''))
                ? array_filter(array_map('trim', explode(',', $args['conditions'])), 'strlen')
                : [];
            $member->setConditions($items);
        }

        // set password explicitly as it's not fillable
        if (isset($this->user_input['no_password'])) {
            $member->is_activated = 0;
        } else {
            if (!$member->exists || !empty($this->user_input['resetpw'])) {
                $member->setPassword($args['password']);
                $member->first_login = Carbon::now();
            }
        }

        if (!$member->exists) {
            $isNewMember = true;
            $member->first_login = new Carbon();
        }

        $clean_address = $this->cleanInputAddress($args, 'billing_');

        $address = $member->addressBilling ?: new MembersAddress();
        $address->type = 'billing';

        $address->fill($clean_address);

        // Double check it's admin whose posting and also that it comes from admin area
        if (!empty($_SESSION['adminId']) && !empty($_POST['from_admin_area'])) {
            // If admin is saving the member
            if (empty($args['passed_id_check'])) {
                $member->passed_id_check = null;
            } elseif (empty($member->passed_id_check)) {
                $member->passed_id_check = new Carbon();
            }
        }

        if (!empty($_SESSION['adminId'])) {
            $member->account_verified = request()->filled('account_verified') ?
                Carbon::now() :
                null;
        }

        $member->save();
        $member->addresses()->save($address);

        $clean_address = $this->cleanInputAddress($args, 'shipping_');

        $address = $member->addressShipping ?: new MembersAddress();
        $address->type = 'shipping';

        $address->fill($clean_address);

        $member->save();
        $member->addresses()->save($address);

        $member->create_nhs_member();
        $member->nhs_member->job_title = request()->input('job_title');
        $member->nhs_member->save();


        Event::dispatch(new SavedMember($this, $this->user_input, $member));

        if ($isNewMember) {
            Event::dispatch(MemberCreatedEvent::class, new MemberCreatedEvent($member));
        } else {
            Event::dispatch(MemberUpdatedEvent::class, new MemberUpdatedEvent($this->oldMember, $member));
        }

        return $member;
    }

    /**
     * Validates self::$user_input and sets error messages if validation fails
     *
     * @param Member $current_member Member being edited
     *
     * @return bool true if no errors were found, false if there are errors
     */
    private function validate($current_member, $allow_no_email = false)
    {
        // The required values - these cannot be empty
        $required = [
            'billing_firstname' => 'first name',
            'billing_lastname'  => 'last name',
            'email'             => 'email address',
            'password'          => 'password',
        ];

        if ($allow_no_email) {
            unset($required['email']);
        }

        if (isset($this->user_input['no_password'])) {
            unset($required['password']);
        }


        if (! $this->user_input['email']) {
            $required['contact_no'] = 'phone number';
        }

        //list of fields to validate as phone numbers
        $phone_number_fields = [
            'contact_no'
        ];

        //When dealing with a US address the state is required
        if ($this->user_input['billing_country'] == 'US' && !empty($states)) {
            $required['billing_state'] = 'state';
        }

        //Required Fields checks
        foreach ($required as $key => $label) {
            $this->user_input[$key] = trim($this->user_input[$key]);
            if (empty($this->user_input[$key])) {
                $text = 'your';

                $this->setError($key, "Please enter {$text} {$label}");
            }
        }

        if (isset($required['email'])) {
            // Email specific checks - only done when email has been altered
            if (!$this->getError('email') && $current_member->email != $this->user_input['email']) {
                if (filter_var($this->user_input['email'], FILTER_VALIDATE_EMAIL) === false) {
                    $this->setError('email', 'The email address you provided is invalid');
                } elseif ($this->memberExists($this->user_input['email'])) {
                    $this->setError('email', 'There is already an account with this email address');
                }
            }
        }

        // Password Specific Check
        if (! isset($this->user_input['no_password'])) {
            if (!$this->getError('password')) {
                //make sure string is alphanumeric and at least 8 chars long
                if (!self::sufficientPasswordStrength($this->user_input['password'])) {
                    $this->setError('password', 'Please enter a password of at least 8 characters using letters as well as numbers');
                } elseif ($this->user_input['password'] != $this->user_input['password2']) {
                    $this->setError('password2', 'The passwords you have entered do not match');
                }
            }
        }

        //ignore password errors when registered user is not wanting to update password

        if (isset($this->user_input['no_password'])) {
            unset($this->errors['password']);
            unset($this->errors['password2']);
        }

        if ($current_member->id && empty($this->user_input['resetpw'])) {
            unset($this->errors['password']);
            unset($this->errors['password2']);
        }

        //validate phone numbers
        foreach ($phone_number_fields as $phone_number_field) {
            if (isset($this->user_input[$phone_number_field]) && preg_match('/[^+0-9() ]/', $this->user_input[$phone_number_field])) {
                $this->setError($phone_number_field, 'You may only use the following characters for phone numbers: + 0-9 ( )');
            }
        }

        // Validate DOB - must be 18 or over
        if (
            !empty($this->user_input['dob_year']) &&
            !empty($this->user_input['dob_month']) &&
            !empty($this->user_input['dob_date'])
        ) {
            try {
                $dob = Carbon::createFromDate(
                    (int) $this->user_input['dob_year'],
                    (int) $this->user_input['dob_month'],
                    (int) $this->user_input['dob_date']
                );
                if ($dob->age < 18) {
                    $this->setError('dob_year', 'You must be at least 18 years old to create an account');
                }
            } catch (\Exception $e) {
                $this->setError('dob_year', 'Please enter a valid date of birth');
            }
        }

        //validate postcode
        if (!$this->getError('billing_postcode') && preg_match('/[^ a-z0-9-]/i', $this->user_input['billing_postcode'])) {
            $this->setError('billing_postcode', 'Please enter a valid post code');
        }

        $dob = null;
        if (
            !empty($this->user_input['dob_year']) &&
            !empty($this->user_input['dob_month']) &&
            !empty($this->user_input['dob_date'])
        ) {
            $dob = $this->user_input['dob_year'] . '-' . $this->user_input['dob_month'] . '-' . $this->user_input['dob_date'];
        }

        $duplicate = Member::memberHasDuplicates([
            'email'             => $this->user_input['email'] ?? null,
            'phone'             => $this->user_input['contact_no'] ?? null,
            'dob'               => $dob,
            'postcode'          => $this->user_input['billing_postcode'] ?? null,
            'firstname'         => $this->user_input['billing_firstname'] ?? null,
            'lastname'          => $this->user_input['billing_lastname'] ?? null,
            'exclude_member_id' => $current_member->id ?: null,
        ]);

        if ($duplicate === 'phone') {
            $this->setError('contact_no', 'Another account already exists with this phone number. Please log in with your existing account, or use a different phone number.');
        } elseif ($duplicate === 'details') {
            $this->setError('billing_firstname', 'An account already appears to exist with these details. If you have previously registered, please log in or contact us for help accessing your account.');
        }

        Event::dispatch(new ValidatingMember($this, $this->user_input));

        //if the errors array is empty the validation is considered passed
        if (count($this->getErrors()) > 0) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * Add an error message
     *
     * @param string $key
     * @param string $message
     */
    public function setError($key, $message)
    {
        $this->errors[$key] = $message;
    }

    /**
     * Get all error messages
     *
     * @return array
     */
    public function getErrors()
    {
        return (array) $this->errors;
    }

    /**
     * Gets the error message associated to a given key
     *
     * @param type $key
     * @return mixed False if key not found, string containing the error message if the key is found
     */
    public function getError($key)
    {
        if (isset($this->getErrors()[$key])) {
            return $this->getErrors()[$key];
        } else {
            return false;
        }
    }

    /**
     * Cleans the address fields as in removes the 'billing_' prefix to make them fillable into a Member
     *
     * @param array $args
     * @return array Same array stripped of billing prefix
     */
    public function cleanInputAddress(array $args, $prefix = '')
    {
        // Nothing to strip
        if (empty($prefix)) {
            return $args;
        }

        foreach ($args as $key => $value) {
            if (strpos($key, $prefix) !== false) {
                unset($args[$key]);
                $args[str_replace($prefix, '', $key)] = $value;
            }
        }
        return $args;
    }

    /**
     * Member::sufficientPasswordStrength()
     *
     * Check and validate if user password is sufficient strength
     *
     * @param string $password password to check
     * @return bool whether password is strong enough
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public static function sufficientPasswordStrength($password)
    {
        return (mb_strlen($password) > 7 // at least 8 chars
            && preg_match('/[0-9]/', $password) // at least number
            && preg_match('/[a-z]/i', $password)// at least one letter
        );
    }

    /**
     * Send verification code
     *
     * @param string $method
     * @param null $destination
     * @return string|void
     * @throws Exception
     */
    public function sendVerification($method = 'sms', $destination = null)
    {

        $code = $this->generateVerificationCode();
        if ($method === 'sms') {
            return $this->sendVerificationSms($code, $destination);

        }
        return $this->sendVerificationEmail($code, $destination);
    }

    /**
     * Generate a verification code
     *
     * @throws Exception
     */
    private function generateVerificationCode()
    {
        $member = Auth::getLoggedInMember();
        $member->verification_code = random_int(100000, 999999);
        $member->verification_code_expiry = Carbon::now()->addMinutes(15);
        $member->save();

        return $member->verification_code;
    }

    /**
     * @param $code
     * @param $contact_number
     * @return string
     * @throws Exception
     */
    private function sendVerificationSms($code, $contact_number)
    {
        $phone_number = str_replace([
            ' ',
            '+'
        ], [
            '',
            '00'
        ], $contact_number);

        if (!is_numeric($phone_number)) {
            throw new Exception('Please enter a valid phone number');
        }

        // Set the code as random 6-digit number
        $message = "{$code} is your " . config('app.name') . ' verification code.';
        return SMS::sendMessage($phone_number, $message);
    }

    /**
     * send verification email
     *
     * @param $code
     * @param $email
     * @return bool|int
     * @throws Exception
     */
    private function sendVerificationEmail($code, $email): bool|int
    {
        $subject = "{$code} is your " . config('app.name') . ' verification code.';
        $content = app('twig')->render('members/emails/verificationEmail.twig', [
             'verification_code' => $code
        ]);
        return email($email, $subject, $content);
    }

    /**
     * Express registration for members
     *
     * @param array $submittedData
     * @return Member|null
     */
    public function saveExpress(array $submittedData): ?Member
    {
        $required = [
            'email',
            'firstname',
            'lastname',
            'phone',
        ];

        foreach ($required as $fieldName) {
            if (empty($submittedData[$fieldName])) {
                $this->setError($fieldName, 'This field is required');
            }
        }
        if (!empty($this->getErrors())) {
            return null;
        }

        if ($this->memberExists($submittedData['email'])) {
            $this->setError('email', 'There is already an account with this email address');
            return null;
        }

        if (!empty($submittedData['phone'])) {
            $duplicate = Member::memberHasDuplicates([
                'email'     => $submittedData['email'],
                'phone'     => $submittedData['phone'],
                'firstname' => $submittedData['firstname'],
                'lastname'  => $submittedData['lastname'],
            ]);

            if ($duplicate === 'phone') {
                $this->setError('phone', 'Another account already exists with this phone number. Please log in with your existing account, or use a different phone number.');
                return null;
            }
        }

        $member = new Member();

        $member->email = $submittedData['email'];
        $member->contact_no = $submittedData['phone'];
        $member->save();

        $member->addresses()
            ->create([
                'type' => MembersAddress::TYPE_BILLING,
                'firstname' => $submittedData['firstname'],
                'lastname' => $submittedData['lastname'],
                'title' => '',
                'address1' => '',
                'address2' => '',
                'city' => '',
                'state' => '',
                'country' => 'GB',
                'postcode' => '',
            ]);

        return $member;
    }
}
