<?php

namespace Mtc\Plugins\MembersMessaging\Classes;

use Baum\Extensions\Eloquent\Model;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Mtc\Core\Admin\User;
use Mtc\Modules\Members\Classes\Auth;
use Mtc\Modules\Members\Models\Member;
use Mtc\Shop\Order;
use function foo\func;

class Thread extends Eloquent
{
    use SoftDeletes;

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'messenger_threads';

    /**
     * The attributes that can be set with Mass Assignment.
     *
     * @var array
     */
    protected $fillable = [
        'id',
        'order_id',
        'member_id',
        'url',
        'assigned',
        'category',
        'assigned_participant_id',
    ];


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

        self::created(function (self $thread) {

            if ($thread->wasInitiatedByAdmin()) { // Assign to creator.
                $admin_user_id = (new \AdminUser())->get_details('id');
            } else { // Assign to configured auto-assign user.
                $admin_user_id = MESSAGING_AUTO_ASSIGN_USER_ID;
            }

            $notify_new_owner = true;
            if ($thread->assigned) {
                $notify_new_owner = false;
            }

            $admin_user = User::query()->find($admin_user_id);
            $admin_user and $thread->assignAdmin($admin_user, $notify_new_owner);

        });
    }

    /**
     * The attributes that should be mutated to dates.
     *
     * @var array
     */
    protected $dates = ['created_at', 'updated_at', 'deleted_at'];

    const MESSAGE_CATEGORIES = [
        0 => 'General Question',
        1 => 'Doctor Query',
    ];

    /**
     * Messages relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     *
     * @codeCoverageIgnore
     */
    public function messages()
    {
        return $this->hasMany(Message::class, 'thread_id', 'id');
    }

    /**
     * Order relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     *
     * @codeCoverageIgnore
     */
    public function order()
    {
        return $this->belongsTo(Order::class, 'order_id');
    }

    /**
     * @param bool $admin_pov
     * @return HasMany
     */
    public function newUnread($admin_pov = false)
    {
        return $this
            ->messages()
            ->whereNull('read_at')
            ->where('admin', $admin_pov ? '0' : '1')
            ->when($admin_pov === false, function ($query) {
                $query->where('private', 0);
            });
    }

    /**
     * @param bool $admin_pov
     * @return bool
     */
    public function hasUnread($admin_pov = false)
    {
        return $this->newUnread($admin_pov)->count() > 0;
    }


    /**
     * @return bool
     */
    public function hasMemberUnread()
    {
        return $this->hasUnread(false);
    }

    /**
     * @return bool
     */
    public function adminUnreadCount()
    {
        return $this->newUnread(true)->count();
    }


    /**
     * @return bool
     */
    public function memberUnreadCount()
    {
        return $this->newUnread(false)->count();
    }

    /**
     * @param null $last_id
     * @return $this|\Illuminate\Database\Eloquent\Collection|Collection|string|static[]
     */
    public function getMessages($last_id = null)
    {
        $messages = $this
            ->messages()
            ->with([
                'user' => function ($user) {
                    $user->select(['id', 'parent_id'])->with([
                        'addressBilling' => function ($address) {
                            $address->select(['member_id', 'firstname', 'lastname']);
                        }
                    ]);
                }
            ]);


        if (isset($last_id) && $last_id > 0) {
            $messages = $messages->where('id', '<', $last_id);
        }

        $messages = $messages
            ->limit(MESSAGES_PER_LOAD)
            ->latest()
            ->get();

        $messages = $messages->each(function ($message) {
            $message->text = $message->parsedText();
            return $message;
        });


        $messages = $messages->toJson();

        return $messages;
    }

    /**
     * Returns the latest message from a thread.
     *
     */
    public function getLatestMessageAttribute()
    {
        return $this->messages()
            ->where('private', '0')
            ->latest()
            ->first();
    }

    public function public_messages() {
        return $this->messages()->where('private', '0')->get();
    }

    /**
     * Participants relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     *
     * @codeCoverageIgnore
     */
    public function participants()
    {
        return $this->hasMany(Participant::class, 'thread_id', 'id');
    }


    public function assigned_participant()
    {
        return $this->belongsTo(Participant::class, 'assigned_participant_id');
    }

    /**
     * User's relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     *
     * @codeCoverageIgnore
     */
    public function users()
    {
        return $this->belongsToMany(Member::class, Participant::class, 'thread_id', 'member_id');
    }


    public function wasInitiatedByAdmin()
    {
        return (bool)$this->order_id;
    }

    /**
     * Relationship with the latest message placed in this thread
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function latestMessage()
    {
        return $this->belongsTo(Message::class, 'latest_message_id');
    }


    public function scopeDecorated(Builder $query)
    {
        $query
            ->join('messenger_messages AS m', 'm.thread_id', '=', 'messenger_threads.id')
            ->join('messenger_participants AS p', 'p.thread_id', '=', 'messenger_threads.id')
            ->groupBy('messenger_threads.id')
            ->select(
                'messenger_threads.*',
                'p.member_id AS user_id',
                'p.admin AS is_admin_user',
                DB::raw('SUM(IF(m.admin = 1 AND m.private = 0, 0, IF(m.read_at IS NULL, 1, 0))) AS unread_flag')
            )
        ;
    }

    /**
     * Attach the latest message to the query
     *
     * @param Builder $query
     * @return mixed
     */
    public function scopeWithLatestMessage(Builder $query)
    {
        $subQuery = DB::table('messenger_messages')
            ->select('messenger_messages.id')
            ->whereRaw('thread_id = messenger_threads.id')
            ->latest()
            ->limit(1);

        $subQueryTime = DB::table('messenger_messages')
            ->select('messenger_messages.created_at')
            ->whereRaw('thread_id = messenger_threads.id')
            ->latest()
            ->limit(1);

        return $query->select('messenger_threads.*')
            ->selectSub($subQuery, 'latest_message_id')
            ->selectSub($subQueryTime, 'latest_message_time')
            ->with('latestMessage');
    }

    /**
     * Returns all of the latest threads by updated_at date.
     *
     * @return mixed
     */
    public static function getAllLatest()
    {
        return self::latest('updated_at');
    }

    /**
     * Returns all threads by subject.
     *
     * @return mixed
     */
    public static function getBySubject($subjectQuery)
    {
        return self::where('subject', 'like', $subjectQuery)
            ->get();
    }

    /**
     * Returns an array of user ids that are associated with the thread.
     *
     * @param null $userId
     *
     * @return array
     */
    public function participantsUserIds($userId = null)
    {
        $users = $this->participants()
            ->withTrashed()
            ->select('user_id')
            ->get()
            ->map(function ($participant) {
                return $participant->user_id;
            });

        if ($userId) {
            $users->push($userId);
        }

        return $users->toArray();
    }

    /**
     * Returns threads for a specific user
     *
     * @param $member_id
     *
     * @return mixed
     */
    public static function scopeForUser($member_id, $admin = null)
    {
        return Thread::getAllLatest()
            ->whereHas('participants', function ($participant) use ($member_id, $admin) {
                $participant->where('member_id', $member_id)
                    ->where('admin', '=', $admin ? '1' : '0');
            })
            ->get()
            ->reject(function ($thread) {
                return $thread->public_messages()->count() === 0;
            });
    }

    public static function scopeForUsers(array $member_ids)
    {
        return Thread::getAllLatest()
            ->whereHas('participants', function ($participant) use ($member_ids) {
                $participant->whereIn('member_id', $member_ids);
            })
            ->get();
    }

    /**
     * Returns threads with new messages that the user is associated with.
     *
     * @param $query
     * @param $userId
     *
     * @return mixed
     */
    public function scopeForUserWithNewMessages($query, $userId)
    {
        $participantTable = Models::table('participants');
        $threadsTable = Models::table('threads');

        return $query->join($participantTable, $this->getQualifiedKeyName(), '=', $participantTable . '.thread_id')
            ->where($participantTable . '.user_id', $userId)
            ->whereNull($participantTable . '.deleted_at')
            ->where(function ($query) use ($participantTable, $threadsTable) {
                $query->where($threadsTable . '.updated_at', '>', $this->getConnection()
                    ->raw($this->getConnection()
                            ->getTablePrefix()
                        . $participantTable
                        . '.last_read'))
                    ->orWhereNull($participantTable . '.last_read');
            })
            ->select($threadsTable . '.*');
    }

    /**
     * Returns threads between given user ids.
     *
     * @param $query
     * @param $participants
     *
     * @return mixed
     */
    public static function scopeBetween(Collection $participants)
    {
        $participants = $participants->map(function ($participant) {
            return $participant->id;
        })->toArray();

        return self::whereHas('participants', function ($q) use ($participants) {
            $q->whereIn('member_id', $participants)
                ->select(DB::raw('DISTINCT(thread_id)'))
                ->groupBy('thread_id')
                ->havingRaw('COUNT(thread_id)=' . count($participants));
        })->first();
    }

    /**
     * Add users to thread as participants.
     *
     * @param array|mixed $userId
     */
    public function addParticipant($member, $admin = 0)
    {
        $participant = Participant::query()->firstOrCreate([
            'member_id' => $member->id,
            'thread_id' => $this->id,
            'admin' => $admin
        ]);

        return $participant;
    }

    /**
     * Assign admin user to conversation thread
     *
     * @param User $user
     */
    public function assignAdmin(User $user, $notify_new_owner = true)
    {
        //Log::debug(__METHOD__ . ' -- notify_new_owner: ' . (int)$notify_new_owner);

        $participant = $this->addParticipant($user, 1);
        $this->assigned = 1;
        $this->assigned_participant_id = $participant->id;
        $this->save();

        $this->markUnread();

        $email_body = template('emails/new_message.twig', [
            'member' => $user,
            'thread' => 1,
            'thread_url' => SITE_URL . '/plugins/MembersMessaging/admin/view_member_thread.php?url=' . $this->url,
        ]);

        if ($notify_new_owner) {
            $email_to = DEV_MODE ? DEV_EMAIL : $user->email;
            email($email_to, "You have been assigned a new message thread on " . config('app.name'), $email_body, ['dev_copy' => DEV_MODE]);
        }
    }

    /**
     * Remove participants from thread.
     *
     * @param array|mixed $userId
     */
    public function removeParticipant($userId)
    {
        $userIds = is_array($userId) ? $userId : (array)func_get_args();

        Models::participant()
            ->where('thread_id', $this->id)
            ->whereIn('user_id', $userIds)
            ->delete();
    }

    /**
     * Adds message notifications for the users needed
     *
     * @param Collection $members
     * @param Message $message
     */
    public function addMessageUnseen(Collection $members, Message $message)
    {

        // if any of the participants is a student -
        // add their parent to notifications
        $members->each(function ($m) use ($members) {
            if ($m->parent_id > 0) {
                $members->push($m->parent()
                    ->first());
            }
        });
        // drop the member sending the message from the
        // notifications
        $members = $members->reject(function ($member) {
            return $member->id == Auth::getLoggedInMember()->id;
        });

        $members->each(function ($member) use ($message) {
            MessagesRead::query()
                ->updateOrCreate([
                    'member_id' => $member->id,
                    'message_id' => $message->id,
                    'thread_id' => $message->thread_id,
                ]);
        });
    }


    public static function messagesUnread()
    {
        return collect(DB::select(DB::raw("
                    SELECT mt.id, mt.url, mm.`text`, COUNT(mt.id) AS total_messages FROM messenger_messages_read mr
                    LEFT JOIN messenger_messages mm ON mr.message_id = mm.id
                    LEFT JOIN messenger_threads mt ON mm.thread_id = mt.id
                    WHERE mr.member_id = :member_id
                    AND mr.read_at < :now_date
                    GROUP BY mt.id
              "), [
            ':member_id' => Auth::getLoggedInMember()->id,
            ':now_date' => new Carbon(),
        ]));
    }

    /**
     * See if the current thread is unread by the user.
     *
     * @param int $userId
     *
     * @return bool
     */
    public function isUnread($userId)
    {
        try {
            $participant = $this->getParticipantFromUser($userId);

            if ($participant->last_read === null || $this->updated_at->gt($participant->last_read)) {
                return true;
            }
        } catch (ModelNotFoundException $e) { // @codeCoverageIgnore
            // do nothing
        }

        return false;
    }

    /**
     * Finds the participant record from a user id.
     *
     * @param $userId
     *
     * @return mixed
     *
     * @throws ModelNotFoundException
     */
    public function getParticipantFromUser($userId)
    {
        return $this->participants()
            ->where('member_id', $userId)
            ->firstOrFail();
    }

    /**
     * Restores all participants within a thread that has a new message.
     */
    public function activateAllParticipants()
    {
        $participants = $this->participants()
            ->withTrashed()
            ->get();
        foreach ($participants as $participant) {
            $participant->restore();
        }
    }

    /**
     * Generates a string of participant information.
     *
     * @param null $userId
     * @param array $columns
     *
     * @return string
     */
    public function participantsString($userId = null, $columns = ['name'])
    {
        $participantsTable = Models::table('participants');
        $usersTable = Models::table('users');
        $userPrimaryKey = Models::user()
            ->getKeyName();

        $selectString = $this->createSelectString($columns);

        $participantNames = $this->getConnection()
            ->table($usersTable)
            ->join($participantsTable, $usersTable . '.' . $userPrimaryKey, '=',
                $participantsTable . '.user_id')
            ->where($participantsTable . '.thread_id', $this->id)
            ->select($this->getConnection()
                ->raw($selectString));

        if ($userId !== null) {
            $participantNames->where($usersTable . '.' . $userPrimaryKey, '!=', $userId);
        }

        return $participantNames->implode('name', ', ');
    }

    /**
     * Checks to see if a user is a current participant of the thread.
     *
     * @param $member_id
     *
     * @return bool
     */
    public function hasParticipant($member_id)
    {
        $participants = $this->participants()
            ->where('member_id', '=', $member_id);

        if ($participants->count() > 0) {
            return true;
        }

        // do a check for parent
        if (Member::find($member_id)->member_type_id == Member::MEMBER_TYPE_PARENT_ID) {
            $students = $this->participants->reject(function ($member) {
                return $member->user->member_type_id !== Member::MEMBER_TYPE_STUDENT_ID;
            });

            if ($students->first()->user->parent_id === $member_id) {
                return true;
            }
        }

        return false;
    }

    public function isParentView($member)
    {
        if ($member->member_type_id == Member::MEMBER_TYPE_PARENT_ID) {
            $participants = $this->participants()
                ->where('member_id', '=', $member->id);

            if ($participants->count() === 0) {
                return true;
            }
        }

        return false;
    }

    public function getOtherParticipant($member)
    {
        return $this->participants->reject(function ($participant) use ($member) {
            return $participant->member_id == $member->id;
        });
    }

    /**
     * Generates a select string used in participantsString().
     *
     * @param $columns
     *
     * @return string
     */
    protected function createSelectString($columns)
    {
        $dbDriver = $this->getConnection()
            ->getDriverName();
        $tablePrefix = $this->getConnection()
            ->getTablePrefix();
        $usersTable = Models::table('users');

        switch ($dbDriver) {
            case 'pgsql':
            case 'sqlite':
                $columnString = implode(" || ' ' || " . $tablePrefix . $usersTable . '.', $columns);
                $selectString = '(' . $tablePrefix . $usersTable . '.' . $columnString . ') as name';
                break;
            case 'sqlsrv':
                $columnString = implode(" + ' ' + " . $tablePrefix . $usersTable . '.', $columns);
                $selectString = '(' . $tablePrefix . $usersTable . '.' . $columnString . ') as name';
                break;
            default:
                $columnString = implode(", ' ', " . $tablePrefix . $usersTable . '.', $columns);
                $selectString = 'concat(' . $tablePrefix . $usersTable . '.' . $columnString . ') as name';
        }

        return $selectString;
    }

    /**
     * Returns array of unread messages in thread for given user.
     *
     * @param $userId
     *
     * @return \Illuminate\Support\Collection
     */
    public function userUnreadMessages($userId)
    {
        $messages = $this->messages()
            ->get();

        try {
            $participant = $this->getParticipantFromUser($userId);
        } catch (ModelNotFoundException $e) {
            return collect();
        }

        if (!$participant->last_read) {
            return $messages;
        }

        return $messages->filter(function ($message) use ($participant) {
            return $message->updated_at->gt($participant->last_read);
        });
    }

    /**
     * Returns count of unread messages in thread for given user.
     *
     * @param $userId
     *
     * @return int
     */
    public function userUnreadMessagesCount($userId)
    {
        return $this->userUnreadMessages($userId)
            ->count();
    }


    public function getCountUnreadMessagesFromCustomer()
    {
        $messages = $this->messages()
                         ->whereNull('read_at')
                         ->where(function($query) {
                             // private messages are normally on behalf of customers
                             $query->where('admin', 0)->orWhere('private', 1);
                         });

        return $messages->count();
    }


    public function hasUnreadMessagesFromCustomer()
    {
        return (bool)$this->getCountUnreadMessagesFromCustomer();
    }


    public function markRead()
    {
        $logged_in_admin_user = Auth::getLoggedInMember()->id;
        $logged_in_admin_user = (new \AdminUser())->get_details('id');

        // Don't mark as read if message read by a different than assigned user.
        if ($logged_in_admin_user != $this->getAssignedUserId()) {
            return;
        }

        $this->messages()
             ->whereNull('read_at')
             ->where(function($query) {
                 $query->where('admin', 0)->orWhere('private', 1);
             })
             ->update(['read_at' => Carbon::now()]);
    }


    private function markUnread()
    {
        $this->messages()
            ->whereNotNull('read_at')
            ->where(function($query) {
                $query->where('admin', 0)->orWhere('private', 1);
            })
            ->update(['read_at' => null]);
    }


    public function lastMessageFrom()
    {
        return $this->messages()
            ->latest()
            ->first()
            ->admin ? 'Customer' : 'Manager';
    }


    public function getAssignedUserId()
    {
        $participant = Participant::find($this->assigned_participant_id);

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

        return $participant->member_id;
    }


    public static function getAllAssignedToUser(?\AdminUser $admin_user = null)
    {
        if (! $admin_user) {
            $admin_user = new \AdminUser();
        }
        $admin_user_id = $admin_user->get_details('id');

        $threads = self::query()
            ->whereHas('assigned_participant', function($query) use ($admin_user_id) {
                $query
                    ->where('member_id' , $admin_user_id)
                    ->where('admin', 1);
            })
            ->get()
        ;

        return $threads;
    }
}
