<?php

namespace Mtc\Modules\BasketRecovery\Classes;

use Carbon\Carbon;
use DateInterval;
use DateTime;
use Exception;
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
use Mtc\Plugins\NHS\Classes\Reminder;
use Mtc\Plugins\SMS\Classes\SMS;
use Mtc\Shop\Category;
use Mtc\Shop\Item;
use Mtc\Shop\Order;

/**
 * BasketRecovery class
 *
 * This eloquent model provides the base for all of the BasketRecovery
 * plugin
 *
 * @author  Craig McCreath <craig.mccreath@mtcmedia.co.uk>
 */
class BasketRecovery extends \Illuminate\Database\Eloquent\Model
{
    const SPECIFIC_PRODUCT = 'specific_product';
    const SPECIFIC_CATEGORY = 'specific_category';

    // Table this class refers to
    protected $table = 'basket_recovery';

    // Columns permitted to be mass assigned
    protected $fillable = [
        'subject',
        'content',
        'sms_content',
        'coupon_data',
        'is_active',
        'interval',
        'interval_seconds',
        'once_per_customer',
        'restriction_id',
        'restriction_type',
        'trigger',
        'type',
        'name',
        'is_button_visible',
        'button_url',
        'button_copy',
    ];

    // Tells eloquent to treat this column differently. In this case, it's treated as an array and automatically json_encode/decode's this.
    protected $casts = [
        'coupon_data' => 'array',
    ];

    // A random hash for any hashing requirements
    const HASH = 'uSq0xO2yWTm129yM3y004ONWAMPJ3auY';

    // MySQL's permitted intervals
    protected static $intervals = [
        'MINUTE',
        'HOUR',
        'DAY',
        'WEEK',
        'MONTH',
    ];

    /**
     * @var string[] list of restrictions supported for emails
     */
    public static $restrictions = [
        'all_products' => 'All Products',
        self::SPECIFIC_PRODUCT => 'A Specific Product',
        self::SPECIFIC_CATEGORY => 'A Specific Category',
    ];

    public static $triggers = [
        'cart' => 'after filling cart details',
        'first_purchase' => 'after placing an order',
        'prescription_reminder' => 'prescription reminder',
    ];

    /**
     * Simple method to get the path to this module, excluding path to public_html
     * @return string
     */
    public static function getBasePath()
    {
        $dir = dirname(__DIR__);

        return str_replace(SITE_PATH, '', $dir);
    }

    /**
     * Eloquent Relationship to the Logs Table
     * @return HasMany
     */
    public function logs()
    {
        return $this->hasMany('Mtc\Modules\BasketRecovery\Classes\BasketRecoveryLog');
    }

    /**
     * Eloquent Relationship to the Logs Table
     * @return HasMany
     */
    public function sentEmails()
    {
        return $this->hasMany(BasketRecoverySentEmails::class, 'recovery_id');
    }

    /**
     * Get a list of the intervals allowed
     * @return array
     */
    public function getIntervalsAllowed()
    {
        return self::$intervals;
    }

    /**
     * Get a list of the restrictions allowed
     * @return array
     */
    public function getRestrictions()
    {
        return self::$restrictions;
    }

    public function getRestrictionName()
    {
        $name = 'N/A';

        if ($this->restriction_type === self::SPECIFIC_PRODUCT) {
            $shop_item = Item::find($this->restriction_id);

            if ($shop_item) {
                $name = "{$shop_item->name}";
            }
        } elseif ($this->restriction_type === self::SPECIFIC_CATEGORY) {
            $category = Category::query()->find($this->restriction_id);

            if ($category) {
                $name = $category->name;
            }
        }

        return $name;
    }

    /**
     * Get a list of the triggers allowed
     * @return array
     */
    public function getTriggers()
    {
        $status_triggers = collect(Order::$statuses)
            ->filter(function ($status, $key) {
                return $key;
            })
            ->keyBy(function ($status, $key) {
                return "status_{$key}";
            })
            ->map(function ($status) {
                return "Status Changed To: {$status}";
            })
            ->toArray();

        return array_merge(self::$triggers, $status_triggers);
    }

    /**
     * Set an interval for this email to run
     * @param int $unit e.g. 2, must be greater than 0
     * @param string $measure e.g. HOUR (list available from getIntervalsAllowed())
     */
    public function setInterval($unit, $measure)
    {
        if (! in_array($measure, self::$intervals)) {
            throw new Exception("Measure not permitted: {$measure}");
        }

        $unit = (int) $unit;
        if ($unit < 1) {
            throw new Exception('Unit must be greater than 0');
        }

        $this->interval = $unit.' '.$measure;
        $this->interval_seconds = strtotime('+'.$this->interval) - time();

        return true;
    }

    /**
     * Get the relevant records to process based on the order type
     *
     * @return array
     */
    public function getOrdersForType()
    {
        if ($this->trigger === 'cart') {
            return $this->getAbandonedOrderIDs();
        }

        if ($this->trigger === 'first_purchase') {
            return $this->getFirstPurchaseOrderIDs();
        }

        if ($this->trigger === 'prescription_reminder') {
            return [];
        }

        if (strpos($this->trigger, 'status') === 0) {
            $status = str_replace('status_', '', $this->trigger);

            return $this->getStatusChangeOrderIDs($status);
        }
    }

    /**
     * Get the list of orders for sending follow-up orders
     *
     * @return array
     */
    private function getPrescriptionReminderOrderIDs($show_full_day = false, $ahead_in_days = 0)
    {
        $due_time = Carbon::now()->addSeconds($this->interval_seconds);

        /*
         * We're combining the query logic for both send-out and upcoming list
         * as this needs to be very similar with only 2 key differences:
         * - upcoming needs to be ahead of send-out X days
         * - we want to see upcoming notifications for all day and not per-minute
         */
        if ($show_full_day) {
            $due_time = $due_time
                ->addDays($ahead_in_days)
                ->format('Y-m-d');
        } else {
            $due_time = $due_time->format('Y-m-d H:i');
        }

        $orders = Order::query()
            ->select('order.id')
            ->distinct()
            ->where('paid', 1)
            ->join('order_items', 'order.id', '=', 'order_items.order_id')
            ->where('order_items.reminder_date', 'like', $due_time.'%');

        return self::filterPaidOrders($orders->get());
    }

    /**
     * Get the list of orders for sending follow-up orders
     *
     * @return array
     */
    private function getFirstPurchaseOrderIDs()
    {
        $due_time = Carbon::now()->subSeconds($this->interval_seconds);

        $orders = Order::query()
            ->select('order.id')
            ->distinct()
            ->where('paid', 1)
            ->where('date', 'like', $due_time->format('Y-m-d H:i').'%')
            ->get();

        return self::filterPaidOrders($orders);
    }

    /**
     * Get the list of orders for sending follow-up orders
     *
     * @return array
     */
    private function getStatusChangeOrderIDs($status)
    {
        $due_time = Carbon::now()->subSeconds($this->interval_seconds);
        $orders = Order::query()
            ->select('order.id')
            ->distinct()
            ->where('paid', 1)
            ->where('order_status.status', $status)
            ->join('order_status', 'order.id', '=', 'order_status.order_id')
            ->where('order_status.timestamp', 'like', $due_time->format('Y-m-d H:i').'%')
            ->get();

        return self::filterPaidOrders($orders);
    }

    /**
     * Apply additional filters for orders
     *
     * @param Collection $orders
     * @return array
     */
    private function filterPaidOrders(Collection $orders)
    {
        $recovery = $this;

        return $orders
            ->filter(function ($order) use ($recovery) {
                // Initialize query
                $query = Order::query()
                    ->where('paid', 1)
                    ->where('order.id', $order->id);

                if ($recovery->restriction_type !== 'all_products') {
                    $query->join('order_items', 'order.id', '=', 'order_items.order_id');

                    if ($recovery->restriction_type === 'specific_category') {
                        $query->join('items_categories', 'items_categories.item_id', '=', 'order_items.item_id')
                            ->whereIn('items_categories.cat_id', get_cat_ids($recovery->restriction_id));
                    } else {
                        $query->where('order_items.item_id', $recovery->restriction_id);
                    }
                }

                // If only once per customer is set we need to check
                // this customer hasn't received this email before
                if ($recovery->once_per_customer) {
                    // Guest checkout - check by order email
                    if (empty($order->member)) {
                        $query->join('order_info', 'order.id', '=', 'order_info.order_id')
                            ->where('email', $order->info['email']);
                    }

                    // registered user order - check by member
                    if (! empty($order->member)) {
                        $query->ofMember($order->member);
                    }

                    return $query->count() == 1;
                }

                return $query->count() > 0;
            })
            ->pluck('id')
            ->toArray();
    }

    /**
     * Get a list of the Orders which are valid for this model's interval
     * This is used once a minute via cron.
     * @return array list of order IDs
     */
    private function getAbandonedOrderIDs()
    {
        $date = new DateTime;
        $date->sub(new DateInterval('PT'.$this->interval_seconds.'S'));

        $collected_orders = Capsule::table('order')
            ->select('order.id', 'order.basket_id')
            ->leftJoin('order_items', function ($join): void {
                $join->on('order.id', '=', 'order_items.order_id');
            })
            ->where('paid', '=', 0)
            ->where('date', 'LIKE', $date->format('Y-m-d H:i').'%')
            ->whereNotNull('order_items.id')
            ->groupBy('order.basket_id');

        if ($this->restriction_type == self::SPECIFIC_PRODUCT) {
            if ($this->restriction_id) {
                $collected_orders->where('order_items.item_id', '=', $this->restriction_id);
            }
        } else if ($this->restriction_type == self::SPECIFIC_CATEGORY) {
            // TODO: SPECIFIC_CATEGORY restriction.
            return [];
        } else {
            // (restriction_type == 'all_products') --> no restrictions required.
        }

        $sql = $collected_orders->toSql();

        $collected_orders = $collected_orders->get();

        $initial_count = $collected_orders->count();

        if ($initial_count > 0) {
            echo $sql;
            echo "\nAfter stage 1:\n";
            var_dump($collected_orders->pluck('id'));
        }

        // try to get paid order of this basket
        // this is needed because if order was made 1h ago, customer went back and forth into basket overview,
        // system creates multiple orders in DB, and if the last one of them is paid (few minutes later)
        // as the cron job runs every minute, it won't catch paid order and will send reminder email to customer which is bad
        if ($collected_orders->count() > 0) {
            foreach ($collected_orders as $key => $order_data) {
                $paid_order = Capsule::table('order')
                    ->select('id')
                    ->where('paid', '=', 1)
                    ->where('basket_id', '=', $order_data->basket_id)
                    ->get();

                if ($paid_order->count() > 0) {
                    // looks like this order is paid though - remove this order form main list and don't send email to customer
                    unset($collected_orders[$key]);
                }
            }
        }

        if ($initial_count > 0) {
            echo "After stage 2:\n";
            var_dump($collected_orders->pluck('id'));
        }

        if ($collected_orders->count() > 0) {
            foreach ($collected_orders as $key => $order_data) {
                $email_sent_order = Capsule::table('basket_recovery_sent_emails')
                    ->select('id')
                    ->where('basket_id', '=', $order_data->basket_id)
                    ->where('recovery_id', '=', $this->id)
                    ->get();

                if ($email_sent_order->count() > 0) {
                    // looks like for this order's basket interval email is already sent - remove this order from main list and don't send this interval email to customer
                    unset($collected_orders[$key]);
                }
            }
        }

        if ($initial_count > 0) {
            echo "After stage 3:\n";
            var_dump($collected_orders->pluck('id'));
        }

        return $collected_orders->pluck('id')->toArray();
    }

    /**
     * Generates the associated coupon for this email, if active and exists.
     * @return mixed array if created, false if not
     */
    public function generateCoupon()
    {
        // No coupon data to generate if type not set or if no values provided for the coupon (either free delivery and/or a value)
        if (
            $this->coupon_data['type'] == ''
            || $this->coupon_data['free_delivery'] == 0 && floatval($this->coupon_data['value']) <= 0
        ) {
            return false;
        }

        $start_date = new DateTime;
        $expiry_date = clone $start_date;
        $expiry_date->add(new DateInterval(config('ecom.followups.coupon.expiry')));

        $code = false;
        while ($code === false) {
            $code = sha1(self::HASH.microtime(true));
            $code = substr($code, 0, 10);
            if (! empty(config('ecom.followups.coupon.prefix'))) {
                $code = config('ecom.followups.coupon.prefix').'-'.$code;
            }

            // Check to see if this code exists. If so, do the loop again until it doesn't.
            if (Capsule::table('coupons')->where('code', '=', $code)->count() > 0) {
                $code = false;
            }
        }

        $data = [
            'code' => $code,
            'from_date' => $start_date,
            'exp_date' => $expiry_date,
            'redemptions' => 1,
            'type' => $this->coupon_data['type'],
            'value' => $this->coupon_data['value'],
            'name' => 'Recovered Basket Coupon - '.$this->subject,
            'min_price' => $this->coupon_data['min_price'],
            'free_delivery' => $this->coupon_data['free_delivery'],
        ];

        Capsule::table('coupons')->insert($data);

        return $data;
    }

    /**
     * Runs the follow-ups
     *
     * @return void
     * @throws Exception
     */
    public static function run(): void
    {
        // Reminders to be sent out daily!!!
        if (date('H:i') === '08:30') {
            Reminder::runBatchSendOut();
        }

        foreach (self::query()->where('is_active', '=', 1)->get() as $recovery) {
            // Get all orders which match the current interval
            /** @var self $recovery */
            $orders = $recovery->getOrdersForType();

            if (! empty($orders)) {
                foreach ($orders as $order_id) {
                    $coupon = [];

                    if (config('ecom.followups.coupon.enabled')) {
                        $coupon = $recovery->generateCoupon();
                    }

                    $order = new \Order($order_id);

                    if (in_array($recovery->type, ['email', 'both'])) {
                        $email = $order->info['email'];
                        $body = BasketRecoveryEmail::generateEmailForBasketId(
                            $order->getBasketId(),
                            $recovery,
                            $coupon,
                        );

                        // save info that current interval basket recovery email was sent to customer based on it's basket ID
                        // that means that next time $recovery->getOrderIDs(); won't catch that basket ID for the same interval email
                        // it will work for another intervals
                        $sentEmailsParams = [
                            'basket_id' => $order->getBasketId(),
                            'recovery_id' => $recovery->id,
                        ];
                        BasketRecoverySentEmails::query()->create($sentEmailsParams);

                        email($email, $recovery->subject, $body);

                        echo "Email\t{$recovery->id}\t{$recovery->interval}\t{$recovery->name}\t{$email}\n";
                    }

                    if (in_array($recovery->type, ['sms', 'both'])) {
                        try {
                            SMS::sendMessage(
                                $order->info['phone_prefix'].$order->info['contact_no'],
                                $recovery->sms_content,
                            );
                        } catch (\Exception $e) {
                            error_log('SMS Follow-up failure: '.$e->getMessage());
                            echo $e->getMessage().PHP_EOL;
                        }
                        echo "SMS\t{$recovery->id}\t{$recovery->interval}\t{$recovery->name}\n";
                    }
                }
            }
        }
    }
}
