<?php

namespace App\Console\Commands;

use App\Modules\Catalog\Models\Category;
use App\Modules\Catalog\Models\Product;
use App\Modules\Catalog\Models\ProductImage;
use App\Modules\User\Models\User;
use App\Modules\Vendor\Models\Vendor;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

/**
 * Imports the Chandra Pharma WooCommerce product export (CSV) into this
 * project's catalog.
 *
 * Source: a WooCommerce "export all columns" dump from chandrapharma.com
 * (1428 rows, mostly junk plugin metadata columns which are ignored here).
 * Decisions below were agreed with the client before writing this:
 *
 *   - Vendor:     every imported product is attached to a new, dedicated
 *                 "Chandra Pharma" vendor — not the generic Test Store.
 *   - Scope:      ALL rows are imported. WooCommerce "Published"=1 rows
 *                 become status=active (live on the storefront);
 *                 everything else (drafts/pending in WooCommerce — that
 *                 was ~96% of the file) becomes status=draft: present in
 *                 the DB and visible in admin, but not live until
 *                 reviewed and published.
 *   - Categories: imported exactly as given in the "Categories" column
 *                 ("Parent > Child" hierarchy, comma-separated for
 *                 multiple). Note: ~95% of rows carry only the single
 *                 category "Digestive Care" in the source data — that
 *                 reads as a default/fallback tag on the WooCommerce
 *                 side, not real per-product categorization. This
 *                 command does not guess a better category; re-tagging
 *                 is an expected follow-up admin task.
 *   - Currency:   source prices are INR. NPR has been fixed-pegged to
 *                 INR at 1:1.6 since 1993, so price/compare_price/mrp
 *                 are each multiplied by 1.6 and stored as NPR.
 *   - Brand:      the CSV's dedicated "Brands" column is 100% empty, and
 *                 the one attribute that looks brand-like ("brands",
 *                 present on ~1.5% of rows) contains placeholder tech
 *                 company names (Apple, Samsung, HTC...) on medicine
 *                 products — bad source data, not usable. brand_id is
 *                 left null for every imported product.
 *   - Images:     product_images rows are created with disk='url'
 *                 (hotlinked to chandrapharma.com), matching this
 *                 project's existing convention — see
 *                 DownloadProductImagesCommand, which is the intended
 *                 next step to pull them into local storage.
 *   - Rx flag:    every product is imported as drug_schedule=OTC,
 *                 is_prescription_required=false. The export has no
 *                 reliable signal for this, and several imported
 *                 products ARE prescription medicines (e.g. diabetes,
 *                 antibiotic, asthma drugs seen in the sample rows) —
 *                 a pharmacist MUST review and correct drug_schedule /
 *                 is_prescription_required before this catalog goes
 *                 live. This command will not guess at medical/legal
 *                 classifications.
 *   - Variable products: the 2 rows with Type=variable have no
 *                 accompanying Type=variation rows in this export (no
 *                 variant data at all), so they're skipped and reported
 *                 rather than silently imported with guessed pricing.
 *
 * Usage:
 *   php artisan import:chandra-products
 *   php artisan import:chandra-products storage/app/imports/chandrapharma-products.csv
 *   php artisan import:chandra-products --descriptions-only
 *
 * Safe to re-run: products are keyed by SKU (or a synthesized "CP-{id}"
 * SKU when the source row had none), so re-running updates existing rows
 * instead of duplicating them.
 *
 * --descriptions-only: re-cleans short_description/description on already
 * -imported products WITHOUT touching vendor/price/stock/categories/images.
 * Use this after a cleanHtml() fix (like the shortcode/literal-"\n" strip
 * added below) instead of a full re-run — a full re-run would also delete
 * and recreate every product_images row as disk='url' again, undoing
 * products:download-images and forcing a full re-download of all 1428
 * images for no reason.
 */
class ImportChandraPharmaProductsCommand extends Command
{
    protected $signature = 'import:chandra-products {csv? : Path to the WooCommerce export CSV} {--descriptions-only : Only re-clean description/short_description text on already-imported products}';

    protected $description = 'Import the Chandra Pharma WooCommerce product export into the catalog';

    private const INR_TO_NPR = 1.6;

    /** @var array<string, Category> keyed by the built "Parent > Child" path */
    private array $categoryCache = [];

    public function handle(): int
    {
        // A console command is one long-lived PHP process for all 1428
        // rows, and this box's default 128M memory_limit isn't enough for
        // that — anything that accumulates per-row (query logs, Telescope
        // watchers if installed, activity-log snapshots) adds up across
        // the whole run and only blows up partway through. Give this
        // command its own generous ceiling rather than requiring a
        // permanent php.ini change.
        ini_set('memory_limit', '1024M');

        // Telescope (present in this project — see the
        // create_telescope_entries_table migration) records every query
        // with full bindings/backtraces in memory for the entire request;
        // for a console command that's the whole import run, and that's
        // almost certainly what actually exhausted memory at ~1000 rows.
        // Stop it recording for the duration of this command.
        if (class_exists(\Laravel\Telescope\Telescope::class)) {
            \Laravel\Telescope\Telescope::stopRecording();
        }

        // Also make sure Eloquent's own query log isn't accumulating
        // (enabled by some local debug setups) for the same reason.
        DB::connection()->disableQueryLog();

        $path = $this->argument('csv') ?? storage_path('app/imports/chandrapharma-products.csv');

        if (! is_file($path)) {
            $this->error("CSV not found at: {$path}");

            return self::FAILURE;
        }

        $handle = fopen($path, 'r');
        if ($handle === false) {
            $this->error('Could not open the CSV file.');

            return self::FAILURE;
        }

        $header = fgetcsv($handle);
        if ($header === false) {
            $this->error('CSV appears to be empty.');
            fclose($handle);

            return self::FAILURE;
        }
        $header = array_map('trim', $header);
        // WooCommerce's export is saved with a UTF-8 BOM, which PHP's
        // fgetcsv() doesn't strip — it ends up glued to the very first
        // header name ("ID" becomes "\xEF\xBB\xBFID"), so every row's
        // $data['ID'] lookup silently came back empty and every row got
        // skipped. Strip it before it's used as an array key.
        if (isset($header[0])) {
            $header[0] = preg_replace('/^\xEF\xBB\xBF/', '', $header[0]);
        }
        $columnCount = count($header);

        $descriptionsOnly = (bool) $this->option('descriptions-only');
        $vendor = $descriptionsOnly ? null : $this->resolveVendor();

        $created = 0;
        $updated = 0;
        $skipped = 0;
        $imagesCreated = 0;
        $variableSkipped = [];

        $this->info('Importing products from ' . $path . ' ...');
        $bar = $this->output->createProgressBar();
        $bar->start();

        while (($row = fgetcsv($handle)) !== false) {
            $bar->advance();

            if (count($row) < $columnCount) {
                $row = array_pad($row, $columnCount, '');
            }
            $data = array_combine($header, array_slice($row, 0, $columnCount));

            $type = trim($data['Type'] ?? '');
            $name = trim($data['Name'] ?? '');
            $wcId = trim($data['ID'] ?? '');

            if ($name === '' || $wcId === '') {
                $skipped++;
                continue;
            }

            if ($type === 'variable') {
                $variableSkipped[] = "#{$wcId} {$name}";
                $skipped++;
                continue;
            }

            $result = $this->importRow($data, $wcId, $vendor, $descriptionsOnly);

            match ($result) {
                'created' => $created++,
                'updated' => $updated++,
                default   => $skipped++,
            };

            $imagesCreated += $this->lastImageCount;

            unset($data, $row);

            // Belt-and-suspenders against whatever's accumulating in
            // memory across the run (see the memory_limit note above) —
            // force PHP to actually reclaim cyclic references (Eloquent
            // models hold circular refs via relations/events) every so
            // often instead of waiting for it to happen on its own.
            if (($created + $updated + $skipped) % 100 === 0) {
                gc_collect_cycles();
            }
        }

        fclose($handle);
        $bar->finish();
        $this->newLine(2);

        $this->info("Products created: {$created} | updated: {$updated} | skipped: {$skipped} | images: {$imagesCreated}");

        if (! empty($variableSkipped)) {
            $this->warn('Skipped ' . count($variableSkipped) . ' "variable" product(s) with no variation data in the export — needs manual entry:');
            foreach ($variableSkipped as $line) {
                $this->line("   - {$line}");
            }
        }

        if ($descriptionsOnly) {
            return self::SUCCESS;
        }

        // Pull the hotlinked images into local storage right away, so the
        // import produces working images end-to-end without a manual
        // second step. Safe to re-run on its own later (e.g. with
        // --force) if some downloads fail here and need a retry.
        if ($imagesCreated > 0) {
            $this->newLine();
            $this->info('Downloading product images into local storage...');
            $this->call('products:download-images');
        }

        $this->newLine();
        $this->warn('Still to do:');
        $this->line('  1. Have a pharmacist review drug_schedule / is_prescription_required — everything imported as OTC by default.');
        $this->line('  2. Re-tag products out of the single "Digestive Care" bucket where the source categorization was a fallback, not real.');
        $this->line('  3. Run php artisan scout:import "App\\Modules\\Catalog\\Models\\Product" if you use search indexing — imports run with search sync suppressed.');
        $this->line('  4. If any images failed to download (reported above), re-run: php artisan products:download-images');

        return self::SUCCESS;
    }

    private int $lastImageCount = 0;

    private function importRow(array $data, string $wcId, ?Vendor $vendor, bool $descriptionsOnly = false): string
    {
        $sku = trim($data['SKU'] ?? '');
        if ($sku === '') {
            $sku = 'CP-' . $wcId;
        }

        $this->lastImageCount = 0;

        if ($descriptionsOnly) {
            $product = Product::where('sku', $sku)->first();
            if (! $product) {
                return 'skipped';
            }

            $shortDescription = $this->cleanHtml($data['Short description'] ?? '');
            $description      = $this->cleanHtml($data['Description'] ?? '');

            Product::withoutSyncingToSearch(function () use ($product, $shortDescription, $description) {
                $product->update([
                    'short_description' => $shortDescription !== '' ? ['en' => $shortDescription] : null,
                    'description'       => $description !== '' ? ['en' => $description] : null,
                ]);
            });

            return 'updated';
        }

        $name = trim($data['Name'] ?? '');
        $published = trim($data['Published'] ?? '') === '1';

        $regularPrice = $this->toFloat($data['Regular price'] ?? null);
        $salePrice    = $this->toFloat($data['Sale price'] ?? null);

        $priceNpr        = $regularPrice !== null ? round(($salePrice ?? $regularPrice) * self::INR_TO_NPR, 2) : null;
        $comparePriceNpr = ($salePrice !== null && $regularPrice !== null) ? round($regularPrice * self::INR_TO_NPR, 2) : null;
        $mrpNpr          = $regularPrice !== null ? round($regularPrice * self::INR_TO_NPR, 2) : null;

        $stockRaw = trim($data['Stock'] ?? '');
        $inStock  = trim($data['In stock?'] ?? '') === '1';
        $stockQuantity = $stockRaw !== '' ? (int) $stockRaw : ($inStock ? 100 : 0);

        $shortDescription = $this->cleanHtml($data['Short description'] ?? '');
        $description      = $this->cleanHtml($data['Description'] ?? '');

        $slug = Str::slug($name) . '-' . $wcId;

        /** @var Product $product */
        $product = Product::withoutSyncingToSearch(function () use (
            $sku, $vendor, $name, $slug, $shortDescription, $description,
            $priceNpr, $comparePriceNpr, $mrpNpr, $stockQuantity, $published,
        ) {
            return Product::updateOrCreate(
                ['sku' => $sku],
                [
                    'vendor_id'                 => $vendor->id,
                    'name'                      => ['en' => $name],
                    'slug'                      => $slug,
                    'short_description'         => $shortDescription !== '' ? ['en' => $shortDescription] : null,
                    'description'               => $description !== '' ? ['en' => $description] : null,
                    'product_type'              => 'simple',
                    'price'                     => $priceNpr,
                    'compare_price'             => $comparePriceNpr,
                    'mrp'                       => $mrpNpr,
                    'currency'                  => 'NPR',
                    'track_inventory'           => true,
                    'stock_quantity'            => $stockQuantity,
                    'status'                    => $published ? 'active' : 'draft',
                    'published_at'              => $published ? now() : null,
                    'drug_schedule'             => 'OTC',
                    'is_prescription_required'  => false,
                    'requires_shipping'         => true,
                ]
            );
        });

        $wasCreated = $product->wasRecentlyCreated;

        // ── Categories ───────────────────────────────────────────────────
        $categoryIds = [];
        foreach (explode(',', $data['Categories'] ?? '') as $path) {
            $path = trim($path);
            if ($path === '') {
                continue;
            }
            $category = $this->resolveCategoryPath($path);
            if ($category) {
                $categoryIds[] = $category->id;
            }
        }
        if (! empty($categoryIds)) {
            $product->categories()->sync($categoryIds);
            $product->forceFill(['category_id' => $categoryIds[0]])->save();
        }

        // ── Images (hotlinked — disk='url', matches
        //    DownloadProductImagesCommand's expected input format) ────────
        $urls = array_values(array_filter(array_map('trim', explode(',', $data['Images'] ?? ''))));
        $this->lastImageCount = 0;
        if (! empty($urls)) {
            $product->images()->delete();
            foreach ($urls as $i => $url) {
                ProductImage::create([
                    'product_id'    => $product->id,
                    'url'           => $url,
                    'url_thumbnail' => $url,
                    'url_medium'    => $url,
                    'url_large'     => $url,
                    'disk'          => 'url',
                    'is_primary'    => $i === 0,
                    'sort_order'    => $i,
                ]);
                $this->lastImageCount++;
            }
        }

        return $wasCreated ? 'created' : 'updated';
    }

    private function resolveVendor(): Vendor
    {
        $owner = User::firstOrCreate(
            ['email' => 'chandrapharma@stpecommerce.com'],
            [
                'name'              => 'Chandra Pharma',
                'password'          => Str::random(32),
                'email_verified_at' => now(),
            ]
        );

        if (method_exists($owner, 'hasRole') && ! $owner->hasRole('vendor')) {
            $owner->assignRole('vendor');
        }

        $vendor = Vendor::firstOrCreate(
            ['email' => 'chandrapharma@stpecommerce.com'],
            [
                'owner_id'        => $owner->id,
                'name'            => 'Chandra Pharma',
                'slug'            => 'chandra-pharma',
                'legal_name'      => 'Chandra Pharma',
                'country'         => 'NP',
                'commission_rate' => 0.00,
                'status'          => 'approved',
                'approved_at'     => now(),
                'is_active'       => true,
            ]
        );

        if (! $owner->vendor_id) {
            $owner->update(['vendor_id' => $vendor->id]);
        }

        return $vendor;
    }

    /**
     * "Health Care > Pain & Fever Relief" resolves/creates "Health Care"
     * as a root category, then "Pain & Fever Relief" under it. A bare
     * "Digestive Care" resolves/creates a root category with no parent.
     */
    private function resolveCategoryPath(string $path): ?Category
    {
        if (isset($this->categoryCache[$path])) {
            return $this->categoryCache[$path];
        }

        $segments = array_values(array_filter(array_map('trim', explode('>', $path))));
        $parent   = null;
        $built    = '';

        foreach ($segments as $depth => $segment) {
            $built = $built === '' ? $segment : $built . ' > ' . $segment;

            if (isset($this->categoryCache[$built])) {
                $parent = $this->categoryCache[$built];
                continue;
            }

            $slug = Str::slug($segment) . ($parent ? '-' . $parent->id : '');

            $category = Category::firstOrCreate(
                ['slug' => $slug],
                [
                    'parent_id'    => $parent?->id,
                    'name'         => ['en' => $segment],
                    'depth'        => $depth,
                    'is_active'    => true,
                    'show_in_menu' => true,
                ]
            );

            $this->categoryCache[$built] = $category;
            $parent = $category;
        }

        return $parent;
    }

    private function toFloat(?string $value): ?float
    {
        $value = trim((string) $value);

        return $value === '' ? null : (float) $value;
    }

    /**
     * The source export's short/long descriptions carry cruft from the
     * old WordPress site's page builder (WPBakery/Visual Composer-style
     * shortcodes like "[vc_row][vc_column][vc_column_text css=""]...
     * [/vc_column_text]") plus literal two-character "\n" escape
     * sequences baked into the text as-is (not real newlines — an
     * artifact of how the exporter serialized multi-line content).
     * Neither is touched by strip_tags() (shortcodes use square brackets,
     * not HTML tags) or by the whitespace-collapsing regex below (a
     * literal backslash+n isn't whitespace), so both need stripping
     * explicitly before the text is safe to store/display as plain text.
     */
    private function cleanHtml(string $html): string
    {
        // Page-builder shortcodes: [tag], [tag attr="value"], [/tag].
        $text = preg_replace('/\[\/?[a-zA-Z_][a-zA-Z0-9_-]*(?:\s+[^\]]*)?\]/', '', $html) ?? $html;

        $text = html_entity_decode(strip_tags($text));

        // Literal "\n" / "\r\n" / "\r" escape sequences left in the text
        // as two-plus literal characters, not actual line breaks.
        $text = str_replace(['\\r\\n', '\\n', '\\r'], ' ', $text);

        $text = preg_replace('/\s+/u', ' ', $text) ?? $text;

        return trim($text);
    }
}
