<?php

/*
 * Plugin Name:         WC Next Sync
 * Plugin URI:          https://gitlab.com/codeccoop/wp/plugins/wc-next-sync/
 * Description:         Live backup WooCommerce orders as XLSX on Nextcloud
 * Author:              codeccoop
 * Author URI:          https://www.codeccoop.org
 * License:             GPLv2 or later
 * License URI:         http://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:         wc-next-sync
 * Domain Path:         /languages
 * Version:             1.2.3
 * Requires Plugins:    woocommerce
 */

namespace WC_NEXT_SYNC;

use WC_NEXT_SYNC\Common\Plugin as Base_Plugin;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use WP_User_Query;
use WP_Query;
use WC_Coupon;
use WC_Customer;

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

require 'vendor/autoload.php';

require_once 'common/class-plugin.php';

require_once 'includes/class-menu.php';
require_once 'includes/class-settings-form.php';
require_once 'includes/class-settings-store.php';

/**
 * WC Next Sync plugin.
 */
class Plugin extends Base_Plugin
{
    /**
     * Handles pending count option name.
     *
     * @var string
     */
    private const PENDING_ORDERS = 'wc_next_sync_pending_orders';

    /**
     * Handles schedule option name
     *
     * @var string
     */
    private const SCHEDULE_HOOK = 'wc_next_sync_orders_schedule';

    /**
     * Handles plugin's menu class.
     *
     * @var string
     */
    protected const menu_class = '\WC_NEXT_SYNC\Menu';

    /**
     * Handles plugin's settings form class.
     *
     * @var string
     */
    protected const settings_form_class = '\WC_NEXT_SYNC\Settings_Form';

    /**
     * Handles plugin's settgins store class.
     *
     * @var string
     */
    protected const store_class = '\WC_NEXT_SYNC\Settings_Store';

    /**
     * Plugin activation callback. Checks if woocommerce is installed and active.
     */
    public static function activate()
    {
        $plugin = plugin_basename(__FILE__);
        if (isset($_GET['plugin']) && $_GET['plugin'] === $plugin) {
            $has_deps = self::is_plugin_active('woocommerce/woocommerce.php');

            if (!$has_deps) {
                add_action('admin_notices', function () {
                    $class = 'notice notice-error';
                    $message = __(
                        'WC Next Sync requires WooCommerce plugin to be installed.',
                        'wc-next-sync'
                    );
                    printf(
                        '<div class="%s"><p>%s</p></div>',
                        esc_attr($class, esc_html($message))
                    );
                });

                add_action('network_admin_notices', function () {
                    $class = 'notice notice-error';
                    $message = __(
                        'WC Next Sync requires WooCommerce plugin to be installed.',
                        'wc-next-sync'
                    );
                    printf(
                        '<div class="%s"><p>%s</p></div>',
                        esc_attr($class, esc_html($message))
                    );
                });

                deactivate_plugins($plugin);
            } else {
                $connection = (array) get_option('wc-next-sync_connection', []);
                self::schedule($connection['schedule'] ?? null);
            }
        }
    }

    /**
     * Plugin deactivation callback. Unschedules orders synchronization.
     */
    public static function deactivate()
    {
        $timestamp = wp_next_scheduled(self::SCHEDULE_HOOK, []);
        if ($timestamp !== false) {
            wp_unschedule_event($timestamp, self::SCHEDULE_HOOK, []);
        }
    }

    /**
     * Schedule synchronization based on plugin settings.
     */
    public static function schedule($schedule = null)
    {
        if (!$schedule) {
            $setting = self::setting('connection');
            if (!$setting->schedule) {
                return;
            }

            $schedule = $setting->schedule;
        }

        $timestamp = wp_next_scheduled(self::SCHEDULE_HOOK, []);
        if ($timestamp) {
            $scheduled = wp_get_schedule(self::SCHEDULE_HOOK, []);
            if ($scheduled !== $schedule) {
                wp_unschedule_event($timestamp, self::SCHEDULE_HOOK, []);
            } else {
                return;
            }
        }

        wp_schedule_event(time(), $schedule, self::SCHEDULE_HOOK, []);
    }

    /**
     * Plugin constructor. Registers custom schedules and bind the custom
     * schedule hook to the sync orders callback.
     */
    protected function construct(...$args)
    {
        parent::construct(...$args);

        add_action(self::SCHEDULE_HOOK, static function () {
            self::sync_orders();
        });

        add_filter('cron_schedules', static function ($schedules) {
            $schedules['minutly'] = [
                'interval' => 60,
                'display' => __('Minutly', 'wc-next-sync'),
            ];
            $schedules['quarterly'] = [
                'interval' => 60 * 15,
                'display' => __('Quarterly', 'wc-next-sync'),
            ];
            $schedules['twicehourly'] = [
                'interval' => 60 * 30,
                'display' => __('Twice an hour', 'wc-next-sync'),
            ];
            return $schedules;
        });

        add_action(
            'updated_option',
            static function ($option) {
                if ($option === self::slug() . '_connection') {
                    self::schedule();
                }
            },
            10,
            1
        );

        add_action(
            'woocommerce_order_status_changed',
            static function ($order_id, $old_status, $new_status) {
                $status = self::setting('orders')->status;
                if (in_array($new_status, $status)) {
                    $count = get_option(self::PENDING_ORDERS, 0);
                    update_option(self::PENDING_ORDERS, $count + 1);
                }
            },
            10,
            3
        );

        add_action(
            'wp_ajax_wc_next_sync_synchronize',
            [self::class, 'ajax_handler'],
            10,
            0
        );
    }

    /**
     * Ajax synchronization requests handler. Allows to manually syncrhonize WooCommerce with Nextlcoud.
     */
    public static function ajax_handler()
    {
        check_ajax_referer('wc-next-sync');
        self::sync_orders(true);

        $referer = sanitize_text_field(wp_unslash($_POST['referer']));
        $redirect = get_admin_url(
            null,
            'options-general.php?page=wc-next-sync&tab=' . $referer
        );
        header('Location: ' . $redirect);
        exit();
    }

    /**
     * Performs a connection check over webdav using based on credential params.
     *
     * @var array $params Array with webdav credentials: nextcloud_url, username and password.
     *
     * @return boolean Check result.
     */
    public static function check_connection($params)
    {
        $base_url = $params['nextcloud_url'] ?? '';
        $username = $params['username'] ?? '';
        $password = $params['password'] ?? '';

        if (empty($base_url) || empty($username) || empty($password)) {
            return false;
        }

        if (!filter_var($base_url, FILTER_VALIDATE_URL)) {
            return false;
        }

        $base_url = preg_replace('/\/+$/', '', $base_url);
        $url = sprintf('%s/remote.php/dav/files/%s/', $base_url, $username);

        $authentication = base64_encode($username . ':' . $password);

        $response = wp_remote_request($url, [
            'method' => 'GET',
            'headers' => [
                'Authorization' => 'Basic ' . $authentication,
            ],
        ]);

        if (is_wp_error($response) || $response['response']['code'] !== 200) {
            return false;
        }

        return true;
    }

    /**
     * Performs a synchronization of the woocommerce orders with the
     * remote spreadhseet file.
     *
     * @param boolean $force Control if synchronization should check for pending orders or not.
     */
    private static function sync_orders($force = false)
    {
        $pendings = (int) get_option(self::PENDING_ORDERS, 0);
        if ($pendings === 0 && !$force) {
            return;
        }

        $connection = self::setting('connection');
        $orders = self::setting('orders');
        $products = self::setting('products');
        $customers = self::setting('customers');
        $coupons = self::setting('coupons');

        $base_url = $connection->nextcloud_url;
        $username = $connection->username;
        $password = $connection->password;
        $filepath = $connection->filepath;

        $from_date = $orders->from_date;
        $status = (array) $orders->status;

        foreach ([$base_url, $username, $password, $filepath] as $param) {
            if (empty($param)) {
                return;
            }
        }

        if (!filter_var($base_url, FILTER_VALIDATE_URL)) {
            return;
        }

        if (empty($status)) {
            return;
        } else {
            $status = array_map(function ($status) {
                return 'wc-' . $status;
            }, $status);
        }

        $spreadsheet = new Spreadsheet();
        $sheet = $spreadsheet->getActiveSheet();

        $tab_index = $spreadsheet->getActiveSheetIndex();

        if ($orders->sync) {
            $sheet->setTitle(__('Orders', 'wc-next-sync'));

            $fields = [];
            foreach ($orders->fields as $field => $config) {
                if ($config['enabled']) {
                    $fields[] = [
                        'label' => $config['alias'] ?: $field,
                        'name' => $field,
                    ];
                }
            }

            $fields = apply_filters('wc_next_sync_order_fields', $fields);
            $orders = self::get_orders_data($status, $from_date);

            self::write_table($sheet, $fields, $orders);

            $tab_index += 1;
        }

        if ($products->sync) {
            if ($tab_index > 0) {
                $sheet = $spreadsheet->createSheet($tab_index);
            }

            $sheet->setTitle(__('Products', 'wc-next-sync'));

            $fields = [];
            foreach ($products->fields as $field => $config) {
                if ($config['enabled']) {
                    $fields[] = [
                        'label' => $config['alias'] ?: $field,
                        'name' => $field,
                    ];
                }
            }

            $fields = apply_filters('wc_next_sync_product_fields', $fields);
            $products = self::get_products_data();

            self::write_table($sheet, $fields, $products);

            $tab_index += 1;
        }

        if ($customers->sync) {
            if ($tab_index > 0) {
                $sheet = $spreadsheet->createSheet($tab_index);
            }

            $sheet = $spreadsheet->createSheet($tab_index);
            $sheet->setTitle(__('Customers', 'wc-next-sync'));

            $fields = [];
            foreach ($customers->fields as $field => $config) {
                if ($config['enabled']) {
                    $fields[] = [
                        'label' => $config['alias'] ?: $field,
                        'name' => $field,
                    ];
                }
            }

            $fields = apply_filters('wc_next_sync_customer_fields', $fields);
            $customers = self::get_customers_data();

            self::write_table($sheet, $fields, $customers);

            $tab_index += 1;
        }

        if ($coupons->sync) {
            if ($tab_index > 0) {
                $sheet = $spreadsheet->createSheet($tab_index);
            }

            $sheet = $spreadsheet->createSheet($tab_index);
            $sheet->setTitle(__('Coupons', 'wc-next-sync'));

            $fields = [];
            foreach ($coupons->fields as $field => $config) {
                if ($config['enabled']) {
                    $fields[] = [
                        'label' => $config['alias'] ?: $field,
                        'name' => $field,
                    ];
                }
            }

            $fields = apply_filters('wc_next_sync_coupon_fields', $fields);
            $coupons = self::get_coupons_data();

            self::write_table($sheet, $fields, $coupons);
        }

        $tmp_dir = get_temp_dir();
        $tmp_file = $tmp_dir . 'orders.xlsx';

        $writer = new Xlsx($spreadsheet);
        $writer->save($tmp_file);

        try {
            $url = sprintf(
                $base_url . '/remote.php/dav/files/%s/%s',
                $username,
                $filepath
            );
            $authentication = base64_encode($username . ':' . $password);

            $res = wp_remote_request($url, [
                'method' => 'PUT',
                'headers' => [
                    'Authorization' => 'Basic ' . $authentication,
                ],
                'body' => file_get_contents($tmp_file),
            ]);

            if (!is_wp_error($res) && $res['response']['code'] === 204) {
                update_option(self::PENDING_ORDERS, 0);
            }
        } finally {
            wp_delete_file($tmp_file);
        }
    }

    /**
     * Write a collection of data to a spreadsheet sheet given a list of fields.
     *
     * @param Sheet $sheet
     * @param array $fields
     * @param array $data
     *
     * @return Sheet
     */
    private static function write_table($sheet, $fields, $data)
    {
        $names = array_column($fields, 'name');
        $headers = array_column($fields, 'label');
        $columns = self::get_columns($names);

        for ($i = 0; $i < count($names); $i++) {
            $col = $columns[$i];
            $sheet->setCellValue("{$col}1", $headers[$i]);
        }

        $row = 2;
        foreach ($data as $datum) {
            $values = [];
            foreach ($names as $name) {
                $values[] = $datum[$name] ?? '';
            }

            for ($i = 0; $i < count($values); $i++) {
                $col = $columns[$i];
                $sheet->setCellValue("{$col}{$row}", $values[$i]);
            }

            $row++;
        }
    }

    /**
     * Order name decorator.
     *
     * @param WC_Order $oder Order instance.
     *
     * @return string
     */
    private static function get_order_name($order)
    {
        return implode(
            ', ',
            array_filter(
                array_map(function ($item) {
                    $product = $item->get_product();
                    if (!$product) {
                        return;
                    }
                    return $product->get_name() .
                        ' (' .
                        $item->get_quantity() .
                        ')';
                }, $order->get_items())
            )
        );
    }

    /**
     * Returns alphabetic spreadsheet column names based on fields length.
     *
     * @param array $fields List of field names.
     *
     * @return array
     */
    private static function get_columns($fields)
    {
        $abc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $len = strlen($abc);

        $columns = [];
        for ($i = 0; $i < count($fields); $i++) {
            if ($i >= $len) {
                $index = $i % $len;
                $columns[] = 'A' . $abc[$index];
            } else {
                $columns[] = $abc[$i];
            }
        }

        return $columns;
    }

    /**
     * Retrives orders from the database and returns them as a collection of serialized arrays.
     *
     * @param array $status List with order status to filter by.
     * @param string $from_data Date to filter orders by.
     *
     * @return array
     */
    private static function get_orders_data($status, $from_date)
    {
        $query = ['status' => $status, 'limit' => -1];

        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $from_date)) {
            $query['date_query'] = [
                'after' => $from_date,
            ];
        }

        $query = apply_filters('wc_next_sync_orders_query', $query);
        $orders = wc_get_orders($query);

        return array_map(function ($order) {
            return self::get_order_data($order);
        }, $orders);
    }

    /**
     * Serialize an order as an array.
     *
     * @param WC_Order $order Order instance.
     *
     * @return array
     */
    private static function get_order_data($order)
    {
        return apply_filters(
            'wc_next_sync_order_data',
            [
                'id' => $order->get_id(),
                'name' => self::get_order_name($order),
                'date' => $order->get_date_modified(),
                'status' => $order->get_status(),
                'taxes' => $order->get_total_tax(),
                'total' => $order->get_total(),
                'coupon_codes' => implode(',', $order->get_coupon_codes()),
                'payment_method' => $order->get_payment_method_title(),
                'billing_first_name' => $order->get_billing_first_name(),
                'billing_last_name' => $order->get_billing_last_name(),
                'billing_email' => $order->get_billing_email(),
                'billing_phone' => $order->get_billing_phone(),
                'billing_address_1' => $order->get_billing_address_1(),
                'billing_address_2' => $order->get_billing_address_2(),
                'billing_city' => $order->get_billing_city(),
                'billing_state' => $order->get_billing_state(),
                'billing_postcode' => $order->get_billing_postcode(),
                'billing_country' => $order->get_billing_country(),
                'billing_company' => $order->get_billing_company(),
                'shipping_first_name' => $order->get_shipping_first_name(),
                'shipping_last_name' => $order->get_shipping_last_name(),
                'shipping_phone' => $order->get_shipping_phone(),
                'shipping_address_1' => $order->get_shipping_address_1(),
                'shipping_address_2' => $order->get_shipping_address_2(),
                'shipping_city' => $order->get_shipping_city(),
                'shipping_state' => $order->get_shipping_state(),
                'shipping_postcode' => $order->get_shipping_postcode(),
                'shipping_country' => $order->get_shipping_country(),
                'shipping_company' => $order->get_shipping_company(),
                'shipping_method' => $order->get_shipping_method(),
            ],
            $order
        );
    }

    /**
     * Retrives products from the database and returns them as a collection of serialized arrays.
     *
     * @return array
     */
    private static function get_products_data()
    {
        $query = apply_filters('wc_next_sync_products_query', [
            'limit' => -1,
            'status' => 'publish',
        ]);

        return array_map(function ($product) {
            return self::get_product_data($product);
        }, wc_get_products($query));
    }

    /**
     * Serialize a product as an array.
     *
     * @param WC_Order $order Order instance.
     *
     * @return array
     */
    private static function get_product_data($product)
    {
        return apply_filters(
            'wc_next_sync_product_data',
            [
                'id' => $product->get_id(),
                'name' => $product->get_name(),
                'sku' => $product->get_sku(),
                'description' => $product->get_description(),
                'short_description' => $product->get_short_description(),
                'price' => $product->get_price(),
                'regular_price' => $product->get_regular_price(),
                'sale_price' => $product->get_sale_price(),
                'date_created' => $product->get_date_created(),
                'date_modified' => $product->get_date_modified(),
                'on_sale_from' => $product->get_date_on_sale_from(),
                'on_sale_to' => $product->get_date_on_sale_to(),
                'tax_class' => $product->get_tax_class(),
                'tax_status' => $product->get_tax_status(),
                'manage_stock' => $product->get_manage_stock(),
                'stock_quantity' => $product->get_stock_quantity(),
                'stock_status' => $product->get_stock_status(),
                'backorders' => $product->get_backorders(),
            ],
            $product
        );
    }

    /**
     * Retrives customers from the database and returns them as a collection of serialized arrays.
     *
     * @return array
     */
    private static function get_customers_data()
    {
        $args = apply_filters('wc_next_sync_customers_query', [
            'role' => 'customer',
        ]);

        $args['fields'] = 'ids';
        $query = new WP_User_Query($args);

        return array_map(function ($user_id) {
            return self::get_customer_data($user_id);
        }, $query->get_results());
    }

    /**
     * Serialize a coupon as an array.
     *
     * @param WC_Order $order Order instance.
     *
     * @return array
     */
    private static function get_customer_data($user_id)
    {
        $customer = new WC_Customer($user_id);

        return apply_filters(
            'wc_next_sync_customer_data',
            [
                'id' => $user_id,
                'first_name' => $customer->get_first_name(),
                'last_name' => $customer->get_last_name(),
                'display_name' => $customer->get_display_name(),
                'username' => $customer->get_username(),
                'email' => $customer->get_email(),
                'date_created' => $customer->get_date_created(),
                'date_modified' => $customer->get_date_modified(),
                'billing_first_name' => $customer->get_billing_first_name(),
                'billing_last_name' => $customer->get_billing_last_name(),
                'billing_address_1' => $customer->get_billing_address_1(),
                'billing_address_2' => $customer->get_billing_address_2(),
                'billing_city' => $customer->get_billing_city(),
                'billing_postcode' => $customer->get_billing_postcode(),
                'billing_state' => $customer->get_billing_state(),
                'billing_country' => $customer->get_billing_country(),
                'billing_phone' => $customer->get_billing_phone(),
                'billing_company' => $customer->get_billing_company(),
                'shipping_first_name' => $customer->get_shipping_first_name(),
                'shipping_last_name' => $customer->get_shipping_last_name(),
                'shipping_address_1' => $customer->get_shipping_address_1(),
                'shipping_address_2' => $customer->get_shipping_address_2(),
                'shipping_city' => $customer->get_shipping_city(),
                'shipping_postcode' => $customer->get_shipping_postcode(),
                'shipping_state' => $customer->get_shipping_state(),
                'shipping_country' => $customer->get_shipping_country(),
                'shipping_phone' => $customer->get_shipping_phone(),
                'shipping_company' => $customer->get_shipping_company(),
                'order_count' => $customer->get_order_count(),
            ],
            $customer
        );
    }

    /**
     * Retrives coupons from the database and returns them as a collection of serialized arrays.
     *
     * @return array
     */
    private static function get_coupons_data()
    {
        $args = apply_filters('wc_next_sync_coupons_query', [
            'post_type' => 'shop_coupon',
            'post_status' => 'publish',
            'posts_per_page' => -1,
        ]);

        $args['fields'] = 'ids';
        $query = new WP_Query($args);

        return array_map(function ($coupon_id) {
            return self::get_coupon_data($coupon_id);
        }, $query->posts);
    }

    private static function get_coupon_data($coupon_id)
    {
        $coupon = new WC_Coupon($coupon_id);

        return apply_filters(
            'wc_next_sync_coupon_data',
            [
                'id' => $coupon_id,
                'code' => $coupon->get_code(),
                'description' => $coupon->get_description(),
                'status' => $coupon->get_status(),
                'amount' => $coupon->get_amount(),
                'minimum_amount' => $coupon->get_minimum_amount(),
                'maximum_amount' => $coupon->get_maximum_amount(),
                'usage_count' => $coupon->get_usage_count(),
                'usage_limit' => $coupon->get_usage_limit(),
                'discount_type' => $coupon->get_discount_type(),
                'individual_use' => $coupon->get_individual_use(),
                'date_created' => $coupon->get_date_created(),
                'date_expires' => $coupon->get_date_expires(),
                'date_modified' => $coupon->get_date_modified(),
            ],
            $coupon
        );
    }
}

Plugin::setup();
