<?php
/**
 * URL updater model with 301 redirect generation
 */
namespace Autogedal\UrlUpdater\Model;

use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Category;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Visibility;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Framework\App\Area;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\State;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException;
use Magento\UrlRewrite\Model\UrlPersistInterface;
use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator;
use Magento\Store\Model\StoreManagerInterface;
use Psr\Log\LoggerInterface;

class UrlUpdater
{
    /**
     * @var ProductRepositoryInterface
     */
    private $productRepository;

    /**
     * @var CategoryRepositoryInterface
     */
    private $categoryRepository;

    /**
     * @var UrlPersistInterface
     */
    private $urlPersist;

    /**
     * @var StoreManagerInterface
     */
    private $storeManager;

    /**
     * @var ProductUrlRewriteGenerator
     */
    private $productUrlRewriteGenerator;

    /**
     * @var CategoryUrlRewriteGenerator
     */
    private $categoryUrlRewriteGenerator;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var State
     */
    private $appState;

    /**
     * @var CollectionFactory
     */
    private $productCollectionFactory;

    /**
     * Constructor
     *
     * @param ProductRepositoryInterface $productRepository
     * @param CategoryRepositoryInterface $categoryRepository
     * @param UrlPersistInterface $urlPersist
     * @param StoreManagerInterface $storeManager
     * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator
     * @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator
     * @param LoggerInterface $logger
     * @param State $appState
     * @param CollectionFactory $productCollectionFactory
     */
    public function __construct(
        ProductRepositoryInterface $productRepository,
        CategoryRepositoryInterface $categoryRepository,
        UrlPersistInterface $urlPersist,
        StoreManagerInterface $storeManager,
        ProductUrlRewriteGenerator $productUrlRewriteGenerator,
        CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator,
        LoggerInterface $logger,
        State $appState,
        CollectionFactory $productCollectionFactory
    ) {
        $this->productRepository = $productRepository;
        $this->categoryRepository = $categoryRepository;
        $this->urlPersist = $urlPersist;
        $this->storeManager = $storeManager;
        $this->productUrlRewriteGenerator = $productUrlRewriteGenerator;
        $this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator;
        $this->logger = $logger;
        $this->appState = $appState;
        $this->productCollectionFactory = $productCollectionFactory;
    }

    /**
     * Update URL for a given entity
     *
     * @param string $entityType
     * @param int $entityId
     * @param string $newUrl
     *
     * @return bool
     * @throws NoSuchEntityException
     */
    public function updateUrl($entityType, $entityId, $newUrl)
    {
        $this->ensureAreaCodeIsSet();

        try {
            $this->logger->info("Starting URL update for {$entityType} #{$entityId} to '{$newUrl}'");

            switch ($entityType) {
                case 'product':
                    return $this->updateProductUrl($entityId, $newUrl);
                case 'category':
                    return $this->updateCategoryUrl($entityId, $newUrl);
                default:
                    throw new \InvalidArgumentException("Unsupported entity type: $entityType");
            }
        } catch (\Exception $e) {
            $this->logger->error('Error updating URL: ' . $e->getMessage(), ['exception' => $e]);
            throw $e;
        }
    }

    /**
     * Ensure area code is set to avoid errors
     *
     * @return void
     */
    private function ensureAreaCodeIsSet()
    {
        try {
            $areaCode = $this->appState->getAreaCode();
        } catch (LocalizedException $exception) {
            try {
                $this->appState->setAreaCode(Area::AREA_ADMINHTML);
            } catch (LocalizedException $e) {
                // Area code already set, just continue
            }
        }
    }

    /**
     * Update URL for product
     *
     * @param int $productId
     * @param string $newUrl
     * @return bool
     * @throws NoSuchEntityException
     */
    private function updateProductUrl($productId, $newUrl)
    {
        $product = $this->productRepository->getById($productId);

        $oldUrlKey = $product->getUrlKey();
        $this->logger->info("Updating product #{$productId} URL from '{$oldUrlKey}' to '{$newUrl}'");

        $product->setUrlKey($newUrl);

        $product->setData('save_rewrites_history', true);

        $this->productRepository->save($product);
        $this->logger->info("Successfully saved product with new URL key");

        $storeIds = $this->getAllStoreIds();
        foreach ($storeIds as $storeId) {
            try {
                $storeProduct = $this->productRepository->getById($productId, false, $storeId);

                if ($storeProduct->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE) {
                    $this->regenerateProductUrls($storeProduct, $storeId);
                    $this->logger->info("Generated URL rewrites for product in store #{$storeId}");
                }
            } catch (\Exception $e) {
                $this->logger->error("Error generating URL rewrites for store #{$storeId}: " . $e->getMessage());
                // Continue with next store
            }
        }

        return true;
    }

    /**
     * Update URL for category
     *
     * @param int $categoryId
     * @param string $newUrl
     * @return bool
     * @throws NoSuchEntityException
     */
    private function updateCategoryUrl($categoryId, $newUrl)
    {
        try {
            $category = $this->categoryRepository->get($categoryId);

            $oldUrlKey = $category->getUrlKey();
            $this->logger->info("Updating category #{$categoryId} URL from '{$oldUrlKey}' to '{$newUrl}'");

            $storeIds = $this->getAllStoreIds();

            $existingUrls = [];
            foreach ($storeIds as $storeId) {
                $urls = $this->captureExistingProductUrls($categoryId, $oldUrlKey, $storeId);
                $existingUrls[$storeId] = $urls;
                $this->logger->info("Captured " . count($urls) . " existing product URLs for store #{$storeId}");
            }

            $category->setUrlKey($newUrl);

            $category->setData('save_rewrites_history', true);

            $this->categoryRepository->save($category);
            $this->logger->info("Successfully saved category with new URL key");

            foreach ($storeIds as $storeId) {
                $this->logger->info("Processing store ID: {$storeId}");

                try {
                    $storeCategory = $this->categoryRepository->get($categoryId, $storeId);

                    $this->regenerateCategoryUrls($storeCategory, $storeId);
                    $this->logger->info("Generated URL rewrites for category in store #{$storeId}");

                    $this->createCategoryUrlRedirect($categoryId, $oldUrlKey, $newUrl, $storeId);

                    $this->regenerateProductUrlsForCategory($storeCategory, $storeId);

                    if (isset($existingUrls[$storeId]) && count($existingUrls[$storeId]) > 0) {
                        $this->createRedirectsFromSnapshot($existingUrls[$storeId], $oldUrlKey, $newUrl, $storeId);
                    }
                } catch (\Exception $storeException) {
                    $this->logger->error(
                        "Error processing store {$storeId}: " . $storeException->getMessage(),
                        ['exception' => $storeException]
                    );
                    // Continue with next store
                }
            }

            return true;
        } catch (\Exception $e) {
            $this->logger->error("Critical error updating category URL: " . $e->getMessage(), ['exception' => $e]);
            throw $e;
        }
    }

    /**
     * Regenerate product URLs
     *
     * @param Product $product
     * @param int $storeId
     *
     * @return void
     * @throws UrlAlreadyExistsException
     */
    private function regenerateProductUrls(Product $product, int $storeId)
    {
        try {
            $product->setStoreId($storeId);
            $productUrls = $this->productUrlRewriteGenerator->generate($product, $storeId);
            $this->urlPersist->replace($productUrls);
        } catch (\Exception $e) {
            $this->logger->error(
                "Error generating product URLs for product #{$product->getId()} in store #{$storeId}: " . $e->getMessage(),
                ['exception' => $e]
            );
            throw $e;
        }
    }

    /**
     * Regenerate category URLs
     *
     * @param Category $category
     * @param int $storeId
     *
     * @return void
     * @throws UrlAlreadyExistsException
     */
    private function regenerateCategoryUrls(Category $category, int $storeId)
    {
        try {
            $category->setStoreId($storeId);
            $categoryUrls = $this->categoryUrlRewriteGenerator->generate($category, $storeId);
            $this->urlPersist->replace($categoryUrls);
        } catch (\Exception $e) {
            $this->logger->error(
                "Error generating category URLs for category #{$category->getId()} in store #{$storeId}: " . $e->getMessage(),
                ['exception' => $e]
            );
            throw $e;
        }
    }

    /**
     * Regenerate product URLs for all products in a category
     *
     * @param Category $category
     * @param int $storeId
     * @return void
     */
    private function regenerateProductUrlsForCategory(Category $category, int $storeId)
    {
        try {
            $categoryId = $category->getId();
            $this->logger->info("Regenerating product URLs for category #{$categoryId} in store #{$storeId}");

            $collection = $this->productCollectionFactory->create();
            $collection->addCategoryFilter($category);
            $collection->addStoreFilter($storeId);
            $collection->addAttributeToSelect('visibility');
            $collection->addAttributeToSelect('url_key');

            $pageSize = 20; // Reduced batch size to avoid memory issues
            $collection->setPageSize($pageSize);

            $pages = $collection->getLastPageNumber();
            $totalProducts = $collection->getSize();
            $this->logger->info("Processing {$totalProducts} products from category {$categoryId} in {$pages} batches");

            for ($currentPage = 1; $currentPage <= $pages; $currentPage++) {
                $this->logger->info("Processing batch {$currentPage} of {$pages}");

                $collection->setCurPage($currentPage);
                $collection->load();

                foreach ($collection as $product) {
                    try {
                        if ($product->getVisibility() == Visibility::VISIBILITY_NOT_VISIBLE) {
                            continue;
                        }

                        $this->logger->info("Processing product #{$product->getId()}");

                        $fullProduct = $this->productRepository->getById($product->getId(), false, $storeId);

                        $fullProduct->setCategoryIds(array_merge($fullProduct->getCategoryIds(), [$categoryId]));

                        $fullProduct->setData('save_rewrites_history', true);
                        $fullProduct->setStoreId($storeId);

                        $productUrls = $this->productUrlRewriteGenerator->generate($fullProduct, $storeId);

                        if (empty($productUrls)) {
                            $this->logger->warning("No URL rewrites generated for product #{$product->getId()}");
                            continue;
                        }

                        $this->logger->info("Generated " . count($productUrls) . " URL rewrites for product #{$product->getId()}");

                        $this->urlPersist->replace($productUrls);

                        $this->logger->info("Updated URL rewrites for product #{$product->getId()}");
                    } catch (\Exception $productException) {
                        $this->logger->error(
                            "Error updating product #{$product->getId()}: " . $productException->getMessage(),
                            ['exception' => $productException]
                        );
                        // Continue with next product
                    }
                }

                $collection->clear();

                // Give the system a chance to clean up memory
                if ($currentPage < $pages) {
                    $this->logger->info("Completed batch {$currentPage} of {$pages}");
                    gc_collect_cycles(); // Force garbage collection
                }
            }

            $this->logger->info("Completed regenerating product URLs for category #{$categoryId}");

        } catch (\Exception $e) {
            $this->logger->error(
                "Error regenerating product URLs for category #{$category->getId()}: " . $e->getMessage(),
                ['exception' => $e]
            );
        }
    }

    /**
     * Capture all existing product URLs for a category before making changes
     *
     * @param int $categoryId
     * @param string $categoryUrlKey
     * @param int $storeId
     * @return array
     */
    private function captureExistingProductUrls($categoryId, $categoryUrlKey, $storeId)
    {
        try {
            $this->logger->info("Capturing existing product URLs for category #{$categoryId} with URL key '{$categoryUrlKey}' in store #{$storeId}");

            $objectManager = ObjectManager::getInstance();
            $resource = $objectManager->get(\Magento\Framework\App\ResourceConnection::class);
            $connection = $resource->getConnection();
            $urlRewriteTable = $resource->getTableName('url_rewrite');

            $select = $connection->select()
                ->from($urlRewriteTable)
                ->where('entity_type = ?', 'product')
                ->where('store_id = ?', $storeId)
                ->where('request_path LIKE ?', $categoryUrlKey . '/%')
                ->where('redirect_type = 0'); // Only get current URLs, not existing redirects

            $rewrites = $connection->fetchAll($select);

            return $rewrites;
        } catch (\Exception $e) {
            $this->logger->error("Error capturing URLs: " . $e->getMessage());
            return [];
        }
    }

    /**
     * Create redirects using the snapshot of URLs captured before changes
     *
     * @param array $existingUrls
     * @param string $oldCategoryUrlKey
     * @param string $newCategoryUrlKey
     * @param int $storeId
     * @return int
     */
    private function createRedirectsFromSnapshot($existingUrls, $oldCategoryUrlKey, $newCategoryUrlKey, $storeId)
    {
        try {
            $count = count($existingUrls);
            $this->logger->info("Creating redirects from snapshot for {$count} URLs in store #{$storeId}");

            $objectManager = ObjectManager::getInstance();
            $resource = $objectManager->get(\Magento\Framework\App\ResourceConnection::class);
            $connection = $resource->getConnection();
            $urlRewriteTable = $resource->getTableName('url_rewrite');

            $created = 0;

            foreach ($existingUrls as $rewrite) {
                $oldPath = $rewrite['request_path'];
                $productId = $rewrite['entity_id'];

                $newPath = str_replace($oldCategoryUrlKey . '/', $newCategoryUrlKey . '/', $oldPath);

                $newPathSelect = $connection->select()
                    ->from($urlRewriteTable)
                    ->where('request_path = ?', $newPath)
                    ->where('store_id = ?', $storeId)
                    ->where('redirect_type = 0');

                $newPathExists = $connection->fetchRow($newPathSelect);

                if (!$newPathExists) {
                    $this->logger->warning("New path '{$newPath}' doesn't exist in rewrites, skipping redirect creation for '{$oldPath}'");
                    continue;
                }

                $existingSelect = $connection->select()
                    ->from($urlRewriteTable)
                    ->where('request_path = ?', $oldPath)
                    ->where('store_id = ?', $storeId)
                    ->where('redirect_type = 301');

                $existing = $connection->fetchRow($existingSelect);

                if ($existing) {
                    $this->logger->info("Redirect already exists for {$oldPath}");
                    continue;
                }

                try {
                    $connection->insert(
                        $urlRewriteTable,
                        [
                            'entity_type' => 'product',
                            'entity_id' => $productId,
                            'request_path' => $oldPath,
                            'target_path' => $newPath,
                            'redirect_type' => 301,
                            'store_id' => $storeId,
                            'is_autogenerated' => 0,
                            'description' => 'Auto-generated for category URL change'
                        ]
                    );

                    $created++;
                    $this->logger->info("Created redirect: {$oldPath} -> {$newPath}");
                } catch (\Exception $insertError) {
                    $this->logger->error("Error creating redirect: " . $insertError->getMessage());
                    // Continue to next redirect
                }
            }

            $this->logger->info("Created {$created} redirects from snapshot for category change in store #{$storeId}");
            return $created;
        } catch (\Exception $e) {
            $this->logger->error("Error creating redirects from snapshot: " . $e->getMessage());
            return 0;
        }
    }

    /**
     * Create 301 redirect for the category URL itself
     *
     * @param int $categoryId
     * @param string $oldUrlKey
     * @param string $newUrlKey
     * @param int $storeId
     * @return bool
     */
    private function createCategoryUrlRedirect($categoryId, $oldUrlKey, $newUrlKey, $storeId)
    {
        try {
            $this->logger->info("Creating 301 redirect for category #{$categoryId} from '{$oldUrlKey}' to '{$newUrlKey}' in store #{$storeId}");

            $objectManager = ObjectManager::getInstance();
            $resource = $objectManager->get(\Magento\Framework\App\ResourceConnection::class);
            $connection = $resource->getConnection();
            $urlRewriteTable = $resource->getTableName('url_rewrite');

            $oldPath = $oldUrlKey . '.html';
            $newPath = $newUrlKey . '.html';

            $existingSelect = $connection->select()
                ->from($urlRewriteTable)
                ->where('request_path = ?', $oldPath)
                ->where('store_id = ?', $storeId)
                ->where('redirect_type = 301');

            $existing = $connection->fetchRow($existingSelect);

            if ($existing) {
                $this->logger->info("Redirect already exists for category URL {$oldPath}");
                return false;
            }

            $connection->insert(
                $urlRewriteTable,
                [
                    'entity_type' => 'category',
                    'entity_id' => $categoryId,
                    'request_path' => $oldPath,
                    'target_path' => $newPath,
                    'redirect_type' => 301,
                    'store_id' => $storeId,
                    'is_autogenerated' => 0,
                    'description' => 'Auto-generated for category URL change'
                ]
            );

            $this->logger->info("Created category URL redirect: {$oldPath} -> {$newPath}");
            return true;
        } catch (\Exception $e) {
            $this->logger->error("Error creating category URL redirect: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Get all store IDs
     *
     * @return array
     */
    private function getAllStoreIds()
    {
        $storeIds = [];
        $stores = $this->storeManager->getStores();
        foreach ($stores as $store) {
            $storeIds[] = $store->getId();
        }
        return $storeIds;
    }
}