#!/usr/bin/env php
<?php
/**
 *
 */
namespace FishPig\DevToolbox;

ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

define('OPT_SOURCE_INSTALL', 'source');
define('OPT_TARGET_PROJECT', 'project');
define('OPT_VERSION_BUMP', 'bump');

try {
    # Find Magento install
    $magentoPath = dirname(dirname(dirname(dirname(dirname(__DIR__)))));
    if (!is_dir($magentoPath) || !is_file($magentoPath . '/app/bootstrap.php')) {
        throw new \Exception('Unable to find Magento 2 installation at ' . $magentoPath);
    }

    chdir($magentoPath);
    require 'app/bootstrap.php';
    \Magento\Framework\App\Bootstrap::create(BP, $_SERVER);

    $args = array_merge(['action' => ''], __process_args($_SERVER['argv']));

    // If arg not set, try looking for shortcut
    if (empty($args['action'])) {
        // class.validate
        if (!empty($args['validate'])) {
            $args['action'] = 'class.validate';
            $args['class'] = $args['validate'];
        } elseif (!empty($args['compare'])) {
            $args['action'] = 'compare.product.data';
            $args['ids'] = explode(',', $args['compare']);
        }

        if (empty($args['action'])) {
            throw new \InvalidArgumentException('No action set.');
        }
    }

    $actionFunc = '\FishPig\DevToolbox\\' . str_replace('.', '_', $args['action']);

    if (!is_callable($actionFunc)) {
        throw new \InvalidArgumentException($actionFunc . ' is not a callable function');
    }

    $actionFunc($args);
} catch (\Exception $e) {
    echo $e->getMessage();
    exit(1);
}

//
function __process_args(array $args)
{
    $params = [];
    foreach ($args as $arg) {
        if (strpos($arg, '--') === 0) {
            if (preg_match('/--([^=]+)(.*)/', $arg, $match)) {
                $params[$match[1]] = trim(ltrim($match[2], '=')) ?: true;
            } else {
                $params[ltrim($arg, '-')] = true;
            }
        }
    }

    return $params;
}

//
function sales_order_reset(array $args)
{
    $orderIds = !empty($args['order-id']) ? explode(',', $args['order-id']) : [];

    if (!($orderIds = array_filter(array_unique($orderIds)))) {
        return;
    }

    $orderRepository = _om()->get(\Magento\Sales\Model\OrderRepository::class);
    $resource = _resource();
    $db = _db();
    $fieldKeywords = ['invoiced', 'canceled', 'refunded', 'shipped'];
    $updateDataArray = function($data, $keywords) {
        $keywordLookupPattern = '/(' . implode('|', $keywords) . ')/';
        $updates = [];
        foreach ($data as $k => $v) {
            if (!is_object($v) && !is_array($v) && preg_match($keywordLookupPattern, $k)) {
                $updates[$k] = null;
            }
        }

        return $updates;
    };

    foreach ($orderIds as $orderId) {
        $order = $orderRepository->get($orderId);
        $cond = 'order_id=' . (int)$order->getId();

        echo '#' . $order->getIncrementId() . ' (' . (int)$order->getId() . ')' . PHP_EOL;

        foreach (['invoice', 'creditmemo', 'shipment'] as $orderEntity) {
            $db->delete($resource->getTableName('sales_' . $orderEntity), $cond);
            $db->delete($resource->getTableName('sales_' . $orderEntity . '_grid'), $cond);
        }

        $db->delete($resource->getTableName('sales_order_status_history'), 'entity_name <> \'order\' AND parent_id=' . (int)$order->getId());

        // Update sales_order
        $db->update(
            $order->getResource()->getMainTable(),
            array_merge([
                    'state' => 'new',
                    'status' => 'pending',
                    'base_total_due' => $order->getData('base_grand_total'),
                    'total_due' => $order->getData('grand_total'),
                    'total_paid' => 0.00
                ],
                $updateDataArray($order->getData(), $fieldKeywords)
            ),
            'entity_id=' . (int)$order->getId()
        );

        // Update sales_order_grid
        $db->update(
            $resource->getTableName('sales_order_grid'),
            [
                'status' => 'pending',
                'base_total_paid' => 0.00,
                'total_paid' => 0.00,
                'total_refunded' => 0.00
            ],
            'entity_id=' . (int)$order->getId()
        );

        // sales_order_item
        $itemData = $db->fetchAll($db->select()->from($resource->getTableName('sales_order_item'), '*')->where($cond));
        foreach ($itemData as $item) {
            if ($updates = $updateDataArray($item, $fieldKeywords)) {
                $db->update(
                    $resource->getTableName('sales_order_item'),
                    $updates,
                    $cond
                );
            }
        }
    }
}


//
function clean_scoped_data(array $args)
{
    $flush = isset($args['flush']);
    $clean = isset($args['clean']);
    $debug = isset($args['debug']);

    if (!$flush && !$clean) {
        throw new \InvalidArgumentException(
            'You must specify either --flush or --clean'
        );
    } elseif ($flush && $clean) {
        throw new \InvalidArgumentException(
            'You cannot specify both --flush and --clean'
        );
    }

    $db = _db();

    foreach (['varchar', 'decimal', 'int', 'datetime'] as $tableType) {
        $table = _table('catalog_product_entity_' . $tableType);

        $scopedData = $db->fetchAll(
            $db->select()->from(
                    ['e' => $table],
                    [
                        'entity_id',
                        'attribute_id',
                        'e_value_id' => 'value_id',
                        'e_value' => 'value'
                    ]
                )->where(
                    'e.store_id=?',
                    0
                )->join(
                    ['scoped' => $table],
                    implode(
                        ' AND ',
                        [
                            'e.entity_id = scoped.entity_id',
                            'e.attribute_id = scoped.attribute_id',
                            'scoped.store_id <> 0',
                            'e.value <> scoped.value'
                        ]
                    ),
                    [
                        'scoped_value_id' => 'value_id',
                        'scoped_value' => 'value',
                        'scoped_store_id' => 'store_id'
                    ]
                )
        );

        $idsToPreserve = [];

        if ($scopedData && $clean) {
            // We are cleaning and these values are scoped and unique so
            // we should not delete these.
            $idsToPreserve = array_column($scopedData, 'scoped_value_id');
        }

        $db->delete(
            $table,
            implode(
                ' AND ',
                array_filter([
                    'store_id <> 0',
                    $idsToPreserve ? $db->quoteInto(
                        'value_id NOT IN (?)',
                        $idsToPreserve
                    ) : null
                ])
            )
        );

        if ($scopedData && $clean && $debug) {
            echo $table . PHP_EOL;
            print_r($scopedData);
        }
    }
}

//
function compare_product_data(array $args)
{
    if (empty($args['ids']) || count($args['ids']) !== 2) {
        throw new \InvalidArgumentException('--ids must contain 2 CSV IDS (eg. --ids=1,2)');
    }

    $ids = array_values(array_unique(array_filter(array_map(
        function ($id) {
            return (int)$id ?: null;
        },
        $args['ids']
    ))));

    if (count($ids) !== 2) {
        throw new \InvalidArgumentException('--ids must contain 2 valid IDs');
    }

    $storeIds = array_unique([
        0,
        isset($args['store']) ? (int)$args['store'] : 0
    ]);

    $ignoreAttributeCodes = array_filter(explode(
        ',',
        $args['ignore-attributes'] ?? ''
    ));

    $ignoreDataTables = array_filter(explode(
        ',',
        $args['ignore-data-tables'] ?? ''
    ));

    $ignoreIndexes = array_filter(explode(
        ',',
        $args['ignore-indexes'] ?? ''
    ));

    $objectManager = \Magento\Framework\App\ObjectManager::getInstance();
    $productRepository = $objectManager->get(\Magento\Catalog\Model\ProductRepository::class);
    $products = [
        $productRepository->getById($ids[0]),
        $productRepository->getById($ids[1]),
    ];

    $db = _db();

    $customerGroupIds = array_filter(
        array_column(
            $objectManager->get(\Magento\Customer\Model\ResourceModel\Group\CollectionFactory::class)->create()->toOptionArray(),
            'value'
        )
    );
    $websiteIds = array_keys(
        $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getWebsites(true)
    );
    $taxClassIds = array_column(
        $objectManager->get(
            \Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory::class
        )->create()->toOptionArray(),
        'value'
    );


    // Helper functions
    $prepareSelect = function($select, $primaryKeyField = 'entity_id') use ($ids) {
        return $select->where($primaryKeyField . ' IN (?)', $ids)
            ->order('FIELD(' . $primaryKeyField . ',' . implode(',', $ids) . ')');
    };

    $prepareResults = function (
        array $results,
        $primaryKeyField = 'entity_id',
        array $fieldsToRemove = []
    ) use ($ids) {
        // Setup correct indexes
        $buffer = [];
        foreach ($results as $result) {
            if ((int)$result[$primaryKeyField] === $ids[0]) {
                $buffer[0] = $result;
            } elseif ((int)$result[$primaryKeyField] === $ids[1]) {
                $buffer[1] = $result;
            }
        }

        foreach ($buffer as $index => $row) {
            foreach ($fieldsToRemove as $field) {
                unset($buffer[$index][$field]);
            }
            unset($buffer[$index][$primaryKeyField]);
        }

        return $buffer;
    };


    $compareArrays = function($results, $type) {
        $type = str_pad($type, 32);
        if (!isset($results[0]) && !isset($results[1])) {
            // Both have no data, so technically the same
            return true;
        } elseif (!isset($results[0]) || !isset($results[1])) {
            $hasOutput = false;
            foreach ([0, 1] as $index) {
                if (!isset($results[$index])) {
                    echo $type . ' ** index=' . $index . ' missing' . PHP_EOL;
                    $hasOutput = true;
                }
            }

            if ($hasOutput) {
                echo str_repeat('-', 64) . "\n";
            }
        } elseif ($diff = array_diff_assoc($results[0], $results[1])) {
            if (count($results[0]) === 1 && count($results[1]) === 1) {
                echo sprintf(
                    '%s %s / %s',
                    $type,
                    array_pop($results[0]),
                    array_pop($results[1])
                ) . "\n";
            } else {
                foreach ($results as $index => $result) {
                    foreach ($result as $column => $value) {
                        if (!isset($diff[$column])) {
                            unset($results[$index][$column]);
                        }
                    }
                }

                echo $type . " DIFF\n\n";

                foreach ([0, 1] as $index) {
                    echo 'Index ' . $index . "\n";
                    foreach ($results[$index] as $column => $value) {
                        echo '- ' . str_pad($column, 18) . ' ' . $value . PHP_EOL;
                    }
                    echo "\n";
                }
            }

            echo str_repeat('-', 64) . "\n";

            return false;
        }

        return true;
    };

    // Check main entity table
    $results = $compareArrays(
        $prepareResults(
            $db->fetchAll(
                $prepareSelect(
                    $db->select()->from(
                        _table('catalog_product_entity'),
                        [
                            'entity_id',
                            'attribute_set_id',
                            'type_id',
                            'has_options',
                            'required_options'
                        ]
                    )
                )
            )
        ),
        'Differences in catalog_product_entity'
    );

    // Now check the entity data type tables
    $dataTables = array_diff(
        ['int', 'varchar', 'decimal'],
        $ignoreDataTables
    );

    foreach ($dataTables as $table) {
        $attributes = $db->fetchPairs(
            $db->select()->distinct()->from(
                ['value' => _table('catalog_product_entity_' . $table)],
                'attribute_id'
            )->where('value.entity_id IN (?)', $ids)
            ->join(
                ['attribute' => _table('eav_attribute')],
                'attribute.attribute_id = value.attribute_id',
                'attribute_code'
            )->where(
                'attribute.attribute_code NOT IN (?)',
                $ignoreAttributeCodes ?: ['sdfsdfsdf']
            )
        );

        foreach ($attributes as $attributeId => $attributeCode) {
            foreach ($storeIds as $storeId) {
                $compareArrays(
                    $prepareResults(
                        $db->fetchAll(
                            $prepareSelect(
                                $db->select()->from(
                                    _table('catalog_product_entity_' . $table),
                                    ['entity_id', 'value']
                                )->where('attribute_id=?', $attributeId)
                                ->where('store_id=?', $storeId)
                            )
                        )
                    ),
                    $attributeCode . ($storeId > 0 ? ' - Store #' . $storeId : '')
                );
            }
        }
    }

    $stockTables = array_diff(
        ['item', 'status', 'status_replica'],
        []
    );

    foreach ($stockTables as $table) {
        $compareArrays(
            $prepareResults(
                $db->fetchAll(
                    $prepareSelect(
                        $db->select()->from(
                            _table('cataloginventory_stock_' . $table),
                            '*'
                        ),
                        'product_id'
                    )
                ),
                'product_id',
                [
                    'item_id',
                    'low_stock_date'
                ]
            ),
            'Stock Table ' . $table
        );
    }

    // Indexes
    $indexes = [];

    if (!in_array('price', $ignoreIndexes)) {
        $indexes['price'] = 'catalog_product_index_price';
        $indexes['price_replica'] = 'catalog_product_index_price_replica';
    }

    foreach ($indexes as $indexType => $table) {
        foreach ($customerGroupIds as $customerGroupId) {
            foreach ($websiteIds as $websiteId) {
                foreach ($taxClassIds as $taxClassId) {
                    $compareArrays(
                        $prepareResults(
                            $db->fetchAll(
                                $prepareSelect(
                                    $db->select()->from(
                                        _table($table),
                                        '*'
                                    )->where(
                                        'customer_group_id=?',
                                        $customerGroupId
                                    )->where(
                                        'website_id=?',
                                        $websiteId
                                    )->where(
                                        'tax_class_id=?',
                                        $taxClassId
                                    )->limit(2)
                                )
                            )
                        ),
                        implode(
                            ' / ',
                            [
                                $indexType,
                                'GRP' . str_pad($customerGroupId, 2),
                                'WEB' . $websiteId,
                                'TX' . $taxClassId
                            ]
                        )
                    );
                }
            }
        }


    }

    if (!isset($args['ignore-website'])) {
        foreach ($websiteIds as $websiteId) {
            $compareArrays(
                $prepareResults(
                    $db->fetchAll(
                        $prepareSelect(
                            $db->select()->from(
                                _table('catalog_product_website'),
                                '*'
                            )->where(
                                'website_id=?',
                                $websiteId
                            )->limit(2),
                            'product_id'
                        )
                    ),
                    'product_id'
                ),
                'Website ' . $websiteId
            );
        }
    }
}

//
function class_validate(array $args)
{
    if (empty($args['class'])) {
        throw new \InvalidArgumentException('--class is not set.');
    }

    $validateClass = function($className, $safety = 10) use (&$validateClass) {
        if (--$safety < 1) {
            throw new \RuntimeException(
                'The safey limit was hit trying to find ' . $className
            );
        }

	    try {
            $objectManager = \Magento\Framework\App\ObjectManager::getInstance();
            $object = $objectManager->get($className);

            echo 'ok';
            //echo $className . ': ' . get_class($object) . PHP_EOL;
        } catch (\ReflectionException $e) {
            echo 'Error: ' . $e->getMessage();
        } catch (\Exception $e) {
            echo $className . ': ';
            echo 'Param Check' . PHP_EOL;

    	    $classReader = $objectManager->get('Magento\Framework\Code\Reader\ClassReader');

            foreach($classReader->getConstructor($className) as $param) {
                if ($param[1]) {
                    $validateClass($param[1], $safety);
                }
            }
        }
    };

    $validateClass($args['class']);
}


function module_create(array $args)
{
    define('TEMPLATE_PATH', dirname(__DIR__) . '/data/module_builder/');

    if (!is_dir(TEMPLATE_PATH)) {
        throw new \RuntimeException(
            sprintf("TEMPLATE_PATH does not exist at '%s'.", TEMPLATE_PATH)
        );
    }

    // Functions
    $get_processed_args = function($options = []) {
        $args = [];

        foreach (array_slice($_SERVER['argv'], 1) as $arg) {
            if (preg_match('/^--([^=]+)(=(.*))?/', $arg, $argMatch)) {
                $key = $argMatch[1];
                if (isset($argMatch[3])) {
                    $value = $argMatch[3];
                } else {
                    $value = true;
                }

                $args[$key] = $value;
            } else {
                throw new \InvalidArgumentException(
                    sprintf(
                        "Unable to progress argument '%s'",
                        $arg
                    )
                );
            }
        }

        $args = array_merge($options['defaults'] ?? [], $args);

        if (!empty($options['callback_before_process']) && is_callable($options['callback_before_process'])) {
            $args = call_user_func($options['callback_before_process'], $args);
        }

        if (!empty($options['validate_path_fields'])) {
            foreach ($options['validate_path_fields'] as $field) {
                if (!empty($args[$field])) {
                    $firstChar = substr($args[$field], 0, 1);

                    if ($firstChar === '~') {
                        throw new \InvalidArgumentException(
                            "Paths that start with ~ are not currently supported."
                        );
                    } elseif ($firstChar !== '/') {
                        $args[$field] = getcwd() . '/' . trim($args[$field]);
                    }
                }
            }
        }

        // Custom filter to stop strict false values being removed
        $args = array_filter(
            $args,
            function ($v) {
                return $v !== '' && $v !== null;
            }
        );

        // Check required arguments
        if (!empty($options['required'])) {
            if ($missingRequiredArgs = array_diff_key(array_flip($options['required']), $args)) {
                throw new \InvalidArgumentException(
                    sprintf(
                        "%d required argument(s) are missing. Missing arguments are: %s",
                        count($missingRequiredArgs),
                        "'" . implode("', '", array_keys($missingRequiredArgs)) . "'"
                    )
                );
            }
        }

        if (!empty($options['callback_after_process']) && is_callable($options['callback_after_process'])) {
            $args = call_user_func($options['callback_after_process'], $args);
        }

        return $args;
    };

    //
    $fishpig_module_creator_create_file = function($sourceFile, array $args) {
        if (!is_file($sourceFile)) {
            throw new \InvalidArgumentException(
                sprintf(
                    "Invalid template '%s' does not exist.",
                    $sourceFile
                )
            );
        }

        $template = (string)file_get_contents($sourceFile);

        if (trim($template) === '') {
            throw new \InvalidArgumentException(
                sprintf(
                    "Template '%s' is empty.",
                    $sourceFile
                )
            );
        }

        foreach ($args as $arg => $value) {
            $findReplace = [
                '%' . $arg . '%' => (string)$value,
                '%' . $arg . '.strtolower%' => strtolower((string)$value),
                '%' . $arg . '.strtoupper%' => strtoupper((string)$value),
            ];

            $template = str_replace(array_keys($findReplace), $findReplace, $template);
        }

        if (preg_match_all('/%[a-z\._]+%/', $template, $missingVariableMatches)) {
            throw new \RuntimeException(
                sprintf(
                    "%d variables missed in template '%s'.",
                    count($missingVariableMatches[0]),
                    "'" . implode("', '", $missingVariableMatches[0]) . "'",
                    $sourceFile
                )
            );
        }

        return $template;
    };

//
    $get_template_source_files = function($path) use (&$get_template_source_files) {
        $path = rtrim($path, '/');
        $files = [];
        if (is_dir($path)) {
            foreach (array_slice(scandir($path), 2) as $item) {
                $absolute = $path . '/' . $item;
                if (is_dir($absolute)) {
                    $files = array_merge($files, $get_template_source_files($absolute));
                } else {
                    $files[] = $absolute;
                }
            }
        }

        return $files;
    };

    $args = $get_processed_args(
        [
            'defaults' => [
                'overwrite' => false,
                'debug-args' => false,
                'help' => false
            ],
            'required' => [
                'target',
                'vendor',
                'module',
                'model',
            ],
            'validate_path_fields' => [
                'target',
            ],
            'callback_before_process' => function(array $args) {
                if (strpos($args['module'], '\\') !== false) {
                    list($vendor, $module) = explode('\\', $args['module']);

                    $args['vendor'] = $vendor;
                    $args['module'] = $module;
                }

                if (empty($args['target']) && !empty($args['vendor']) && !empty($args['module'])) {
                    $args['target'] = 'app/code/' . $args['vendor'] . '/' . $args['module'];
                }

                return $args;
            },
            'callback_after_process' => function(array $args) {
                $args = array_merge(
                    [
                        'php_file_header' => "/**\n * @url FishPig.com\n */\ndeclare(strict_types=1);\n",
                        'namespace' => $args['vendor'] . '\\' . $args['module'],
                        'module_name' => $args['vendor'] . '_' . $args['module'],
                        'admin_route' => $args['admin-route'] ?? strtolower($args['module']),
                        'model_id_field' => $args['model-id-field-' . $args['model'] ] ?? strtolower($args['model']) . '_id',
                        'model_layout_prefix' => strtolower($args['module']) . '_' . strtolower($args['model']),
                    ],
                    $args
                );

                return $args;
            }
        ]
    );

    if ($args['debug-args']) {
        print_r($args);
        exit;
    }

    if ($args['help']) {
        echo sprintf(
            "%s --module=Vendor\\Module --model=SomeModel",
            $argv[0]
        );
        exit(0);
    }

    if (is_dir($args['target'])) {
        if ((int)$args['overwrite'] !== 1) {
            throw new \RuntimeException(
                sprintf(
                    "Target directory (%s) exists but the --overwrite argument was not passed.",
                    $args['target']
                )
            );
        }

        if (count(scandir($args['target'])) === 2) {
            // Dir is empty
            rmdir($args['target']);
        } elseif (!realpath($args['target']) || strpos(realpath($args['target']), getcwd()) !== 0) {
            throw new \RuntimeException(
                sprintf(
                    "Target directory (%s) exists but cannot be deleted as the path is not a child of the current directory.",
                    $args['target']
                )
            );
        } else {
            shell_exec('rm -rf ' . realpath($args['target']));
        }
    }

    mkdir($args['target']);

    if (!is_dir($args['target'])) {
        throw new \RuntimeException(
            sprintf(
                "Unable to create target directory at '%s'.",
                $args['target']
            )
        );
    }

    if (!($targetDir = realpath($args['target']))) {
        throw new \RuntimeException(
            sprintf(
                "Error validating target path '%s'.",
                $args['target']
            )
        );
    }

    $files = [];

    $findReplaceInPaths = [
        '_Module_' => $args['module'],
        '_module_' => strtolower($args['module']),
        '_Model_' => $args['model'],
        '_model_' => strtolower($args['model']),
    ];

    foreach ($get_template_source_files(TEMPLATE_PATH) as $sourceFile) {
        $code = $fishpig_module_creator_create_file($sourceFile, $args);

        $absoluteTargetFile = str_replace(
            array_keys($findReplaceInPaths),
            $findReplaceInPaths,
            $targetDir . '/' . str_replace(TEMPLATE_PATH, '', $sourceFile)
        );

        $absoluteTargetPath = dirname($absoluteTargetFile);

        if (!is_dir($absoluteTargetPath)) {
            mkdir($absoluteTargetPath, 0755, true);

            if (!is_dir($absoluteTargetPath)) {
                throw new \RuntimeException(
                    sprintf(
                        "Error creating file directory '%s' at '%s'.",
                        basename($absoluteTargetFile),
                        $absoluteTargetPath
                    )
                );
            }
        }

        file_put_contents($absoluteTargetFile, $code);

        if (!is_file($absoluteTargetFile)) {
            throw new \RuntimeException(
                sprintf(
                    "Error creating file '%s'.",
                    $absoluteTargetFile
                )
            );
        }
    }
}

//
function _om() {
    return \Magento\Framework\App\ObjectManager::getInstance();
}

function _resource() {
    return _om()->get(\Magento\Framework\App\ResourceConnection::class);
}

function _db($c = '') {
    return _resource()->getConnection($c);
}

function _table($table) {
    return _resource()->getTableName($table);
}