<?php

namespace PortalPeople\shippingcosts\components;

use Yii;
use backend\models\Accounts;
use PortalPeople\shippingcosts\models\ColumnMapForm;
use PortalPeople\shippingcosts\models\Shippingcosts;

/**
 * PriceImporter
 *
 * Iterates rows stored in session (produced by PriceConsolidator::toNamedArray()),
 * applies a ColumnMapForm assignment map, and upserts Shippingcosts records.
 *
 * Per-haulier strategy:
 *   - If the number of DB records for a haulier differs from the number of import rows
 *     → delete all existing records for that haulier and reimport fresh.
 *   - If the counts match → match each import row to an existing record by service type
 *     and update in place (postcodes, prices, notes etc. are overwritten).
 *
 * Price columns are parsed with ColumnMapForm::parsePriceHeader(); the cell value
 * becomes Shippingcostsprices::$price.
 */
class PriceImporter
{
    /** Status set on every new Shippingcosts record. */
    public string $defaultStatus = 'active';

    /** Number of times to retry a failed save before giving up. */
    public int $maxRetries = 2;

    /** @var array<string, string>  header → role */
    private array $assignments;

    /** @var array<string, array{label: string, size: float|null, qty: int|null}> */
    private array $priceColumnMeta = [];

    /** @var array<string, int|null>  haulier name → intid memo cache */
    private array $haulierCache = [];

    public function __construct(array $assignments)
    {
        $this->assignments = $assignments;

        foreach ($assignments as $header => $role) {
            if ($role === ColumnMapForm::ROLE_PRICE) {
                $this->priceColumnMeta[$header] = ColumnMapForm::parsePriceHeader($header);
            }
        }
    }

    // ─── Public API ────────────────────────────────────────────────────────

    /**
     * Import all rows with upsert logic grouped per haulier.
     *
     * @param  list<array<string, mixed>> $rows  Output of PriceConsolidator::toNamedArray()
     * @return array{saved: int, updated: int, deleted: int, skipped: int, errors: list<array{row: array, errors: array}>}
     */
    public function import(array $rows): array
    {
        $results = ['saved' => 0, 'updated' => 0, 'deleted' => 0, 'skipped' => 0, 'errors' => []];

        // Parse all rows upfront (resolves haulier IDs, prices, etc.)
        $parsedRows = array_map(fn($row) => $this->parseRow($row), $rows);

        // Group by haulierid; use '__null__' key for rows with no haulier
        $byHaulier = [];
        foreach ($parsedRows as $parsed) {
            $key = $parsed['haulierid'] !== null ? (string) $parsed['haulierid'] : '__null__';
            $byHaulier[$key][] = $parsed;
        }

        foreach ($byHaulier as $haulierKey => $group) {
            $haulierid   = $haulierKey === '__null__' ? null : (int) $haulierKey;
            $importCount = count($group);
            $dbCount     = $this->dbCountForHaulier($haulierid);

            if ($dbCount !== $importCount) {
                // ── Counts differ: nuke existing records and reimport ──
                $results['deleted'] += $this->deleteForHaulier($haulierid);

                foreach ($group as $parsed) {
                    $outcome = $this->saveNew($parsed);
                    if ($outcome === true) {
                        $results['saved']++;
                    } else {
                        $results['skipped']++;
                        $results['errors'][] = $outcome;
                    }
                }
            } else {
                // ── Counts match: update in place, matched by service type ──
                $existing = $this->loadForHaulier($haulierid);

                foreach ($group as $parsed) {
                    $match   = $this->matchByType($existing, $parsed['type']);
                    $outcome = $this->saveOrUpdate($parsed, $match);

                    if ($outcome === 'updated') {
                        $results['updated']++;
                    } elseif ($outcome === true) {
                        $results['saved']++;
                    } else {
                        $results['skipped']++;
                        $results['errors'][] = $outcome;
                    }
                }
            }
        }

        return $results;
    }

    // ─── Parsing ───────────────────────────────────────────────────────────

    /**
     * Convert one raw spreadsheet row into a structured array ready for
     * applying to a Shippingcosts model.
     */
    private function parseRow(array $row): array
    {
        $parsed = [
            'haulierid' => null,
            'type'      => [],
            'postcodes' => '',
            'notes'     => '',
            'prices'    => [],
            '_raw'      => $row,
        ];

        foreach ($this->assignments as $header => $role) {
            $value = $row[$header] ?? null;

            switch ($role) {
                case ColumnMapForm::ROLE_POSTCODES:
                    $parsed['postcodes'] = (string) $value;
                    break;

                case ColumnMapForm::ROLE_TYPE:
                    $parsed['type'] = ($value !== null && $value !== '')
                        ? ColumnMapForm::TYPE_MAP[strtolower(trim((string) $value))] ?? (string) $value
                        : '';
                    break;

                case ColumnMapForm::ROLE_NOTES:
                    $parsed['notes'] = (string) $value;
                    break;

                case ColumnMapForm::ROLE_HAULIER:
                    $parsed['haulierid'] = $this->resolveHaulier($value);
                    break;

                case ColumnMapForm::ROLE_PRICE:
                    if ($value !== null && $value !== '') {
                        $meta           = $this->priceColumnMeta[$header];
                        $parsed['prices'][] = [
                            'label' => $meta['label'],
                            'size' => $meta['size'],
                            'qty'   => $meta['qty'] ?? 1,
                            'price' => (float) $value,
                        ];
                    }
                    break;
            }
        }

        return $parsed;
    }

    // ─── DB helpers ────────────────────────────────────────────────────────

    private function dbCountForHaulier(?int $haulierid): int
    {
        return (int) Shippingcosts::find()
            ->where($haulierid !== null ? ['haulierid' => $haulierid] : ['haulierid' => null])
            ->count();
    }

    /** @return Shippingcosts[] */
    private function loadForHaulier(?int $haulierid): array
    {
        return Shippingcosts::find()
            ->where($haulierid !== null ? ['haulierid' => $haulierid] : ['haulierid' => null])
            ->all();
    }

    /** Delete all records for a haulier via model->delete() to respect lifecycle hooks. */
    private function deleteForHaulier(?int $haulierid): int
    {
        $models = $this->loadForHaulier($haulierid);
        $count  = 0;

        foreach ($models as $model) {
            if ($model->delete() !== false) {
                $count++;
            }
        }

        return $count;
    }

    // ─── Type matching ─────────────────────────────────────────────────────

    /**
     * Find the best-matching model from $existing by comparing type arrays.
     * Returns null if no match is found.
     *
     * @param  Shippingcosts[] $existing
     * @param  string[]        $importType
     */
    private function matchByType(array $existing, string $importType): ?Shippingcosts
    {
        $normalise = static function (mixed $t): string {
            if (is_array($t)) {
                return strtolower(trim(implode('|', array_map('trim', $t))));
            }
            return strtolower(trim((string) ($t ?? '')));
        };

        $needle = $normalise($importType);

        foreach ($existing as $model) {
            if ($normalise($model->type) === $needle) {
                return $model;
            }
        }

        return null;
    }

    // ─── Save / Update ─────────────────────────────────────────────────────

    /**
     * @return true|array{row: array, errors: array}
     */
    private function saveNew(array $parsed): true|array
    {
        $model = new Shippingcosts();
        $model->status = $this->defaultStatus;
        return $this->applyAndSave($model, $parsed);
    }

    /**
     * @return 'updated'|true|array{row: array, errors: array}
     */
    private function saveOrUpdate(array $parsed, ?Shippingcosts $existing): mixed
    {
        if ($existing !== null) {
            $outcome = $this->applyAndSave($existing, $parsed);
            return $outcome === true ? 'updated' : $outcome;
        }

        return $this->saveNew($parsed);
    }

    /**
     * Apply parsed data to a model (new or existing) and call save(), retrying
     * up to $maxRetries times on failure before returning the error array.
     *
     * @return true|array{row: array, errors: array}
     */
    private function applyAndSave(Shippingcosts $model, array $parsed): true|array
    {
        $model->haulierid    = $parsed['haulierid'];
        $model->type         = $parsed['type'];
        $model->postcodes    = $parsed['postcodes'];
        $model->notes        = $parsed['notes'];
        $model->prices       = $parsed['prices'];
        $model->no_of_prices = count($parsed['prices']);

        $attempts   = 0;
        $lastErrors = [];

        do {
            $attempts++;
            try {
                if ($model->save()) {
                    return true;
                }

                $lastErrors = $model->getErrors();
                Yii::warning(
                    "PriceImporter: save attempt {$attempts} failed – "
                        . json_encode($lastErrors)
                        . ' | row: ' . json_encode($parsed['_raw']),
                    __CLASS__
                );
            } catch (\Throwable $e) {
                $lastErrors = ['exception' => [$e->getMessage()]];
                Yii::warning(
                    "PriceImporter: save attempt {$attempts} threw exception – "
                        . $e->getMessage()
                        . ' | row: ' . json_encode($parsed['_raw']),
                    __CLASS__
                );
            }

            $model->clearErrors();
        } while ($attempts <= $this->maxRetries);

        return ['row' => $parsed['_raw'], 'errors' => $lastErrors];
    }

    // ─── Haulier resolution ────────────────────────────────────────────────

    private function resolveHaulier(mixed $value): ?int
    {
        if ($value === null || $value === '') {
            return null;
        }

        if (is_numeric($value)) {
            return (int) $value;
        }

        $name = (string) $value;

        if (array_key_exists($name, $this->haulierCache)) {
            return $this->haulierCache[$name];
        }

        $account = Accounts::find()
            ->where(['name' => $name, 'type' => 'haulier'])
            ->one();

        $intid = $account ? (int) $account->intid : null;
        $this->haulierCache[$name] = $intid;

        return $intid;
    }
}

