<?php

declare(strict_types=1);

namespace App\Libraries;

/**
 * 통계 분석 관리 (전년대비 / 월별·계절별 추이)
 *
 * 월별·계절별 추이·전년대비: bs_type = sale 판매량·판매금액만 집계 (반품·취소 제외)
 */
class BagAnalyticsReportBuilder
{
    private \CodeIgniter\Database\BaseConnection $db;

    /** @var array<string, string> */
    private array $bagNames = [];

    /** 판매(bs_type=sale) 낱장 수량만 합산 */
    private function saleQtySql(): string
    {
        return "CASE WHEN bs.bs_type = 'sale' THEN ABS(bs.bs_qty) ELSE 0 END";
    }

    /**
     * @return array<string, array{label: string, months_label: string, months: list<int>, cross_year: bool}>
     */
    public static function seasonCatalog(): array
    {
        return [
            'spring' => ['label' => '봄', 'months_label' => '3~5월', 'months' => [3, 4, 5], 'cross_year' => false],
            'summer' => ['label' => '여름', 'months_label' => '6~8월', 'months' => [6, 7, 8], 'cross_year' => false],
            'autumn' => ['label' => '가을', 'months_label' => '9~11월', 'months' => [9, 10, 11], 'cross_year' => false],
            'winter' => ['label' => '겨울', 'months_label' => '전년12·1~2월', 'months' => [12, 1, 2], 'cross_year' => true],
        ];
    }

    public static function normalizeSeason(string $season): string
    {
        $raw = trim($season);
        $aliases = [
            '봄' => 'spring',
            '여름' => 'summer',
            '가을' => 'autumn',
            '겨울' => 'winter',
        ];
        $key = $aliases[$raw] ?? strtolower($raw);
        $catalog = self::seasonCatalog();

        return isset($catalog[$key]) ? $key : 'spring';
    }

    public function __construct(?\CodeIgniter\Database\BaseConnection $db = null)
    {
        $this->db = $db ?? \Config\Database::connect();
    }

    /**
     * @return array{
     *   gugunOptions: list<array{code: string, name: string}>,
     *   agencies: list<object>,
     *   lgName: string,
     *   gugunLabel: string
     * }
     */
    public function loadFilterOptions(int $lgIdx): array
    {
        $lgRow = model(\App\Models\LocalGovernmentModel::class)->find($lgIdx);
        $lgName = $lgRow ? trim((string) ($lgRow->lg_name ?? '')) : '';

        $gugunRows = $this->db->query("
            SELECT DISTINCT ds_gugun_code AS code
            FROM designated_shop
            WHERE ds_lg_idx = ? AND ds_gugun_code != ''
            ORDER BY ds_gugun_code
        ", [$lgIdx])->getResultArray();

        $gugunOptions = [['code' => '', 'name' => '전체']];
        foreach ($gugunRows as $row) {
            $code = trim((string) ($row['code'] ?? ''));
            if ($code === '') {
                continue;
            }
            $gugunOptions[] = ['code' => $code, 'name' => $this->gugunLabel($lgIdx, $code)];
        }

        $agencies = model(\App\Models\SalesAgencyModel::class)
            ->where('sa_lg_idx', $lgIdx)
            ->orderForDisplay()
            ->findAll();

        return [
            'gugunOptions' => $gugunOptions,
            'agencies'     => $agencies,
            'lgName'       => $lgName,
            'gugunLabel'   => '',
        ];
    }

    /**
     * @return array{
     *   rows: list<array<string, mixed>>,
     *   months: list<int>,
     *   prevYear: int,
     *   year: int
     * }
     */
    public function buildYearOverYear(
        int $lgIdx,
        int $year,
        string $gugunCode,
        int $dsIdx,
        bool $queried
    ): array {
        $prevYear = $year - 1;
        $months = range(1, 12);
        if (! $queried) {
            return ['rows' => [], 'months' => $months, 'prevYear' => $prevYear, 'year' => $year];
        }

        $this->loadBagNames($lgIdx);
        $agg = $this->aggregateMonthlyByBag($lgIdx, $prevYear, $year, $gugunCode, $dsIdx);

        $rows = [];
        $codesFromAgg = array_map(static fn ($c): string => (string) $c, array_keys($agg));
        foreach ($this->bagCodesForReport($lgIdx, $codesFromAgg) as $code) {
            $code = (string) $code;
            $name = $this->resolveBagName($code);
            $rows[] = $this->yoyBlock($code, (string) $name, '수량', $agg, $prevYear, $year, $months, false);
            $rows[] = $this->yoyBlock($code, (string) $name, '금액', $agg, $prevYear, $year, $months, true);
        }

        return ['rows' => $rows, 'months' => $months, 'prevYear' => $prevYear, 'year' => $year];
    }

    /**
     * @return array{rows: list<array<string, mixed>>, meta: array<string, int>}
     */
    public function buildMonthlyTrend(
        int $lgIdx,
        string $baseYm,
        string $trendBasis,
        float $deviationMin,
        string $gugunCode,
        int $saIdx,
        bool $queried
    ): array {
        $empty = ['rows' => [], 'meta' => ['shopCount' => 0, 'monthSalesShops' => 0]];
        if (! $queried || ! preg_match('/^(\d{4})-(\d{2})$/', $baseYm, $m)) {
            return $empty;
        }

        $year = (int) $m[1];
        $month = (int) $m[2];
        $shops = $this->loadShops($lgIdx, $gugunCode, $saIdx);
        if ($shops === []) {
            return $empty;
        }

        $monthlyByShop = $this->monthlyNetByShop($lgIdx, $year, $month, $gugunCode, $saIdx);
        $avgByShop = $this->averageNetByShop($lgIdx, $year - 1, $gugunCode, $trendBasis, $month, $saIdx);
        $prevYearSameMonth = $trendBasis === 'year_avg'
            ? $this->monthlyNetByShop($lgIdx, $year - 1, $month, $gugunCode, $saIdx)
            : [];

        $rows = [];
        $monthSalesShops = 0;
        foreach ($shops as $shop) {
            $sid = (int) ($shop['ds_idx'] ?? 0);
            $monthly = (float) ($monthlyByShop[$sid] ?? 0.0);
            $avg = (float) ($avgByShop[$sid] ?? 0.0);
            if ($trendBasis === 'year_avg' && $avg <= 0) {
                $avg = (float) ($prevYearSameMonth[$sid] ?? 0.0);
            }
            if ($monthly > 0) {
                $monthSalesShops++;
            }
            $diff = $monthly - $avg;
            $pct = $avg > 0 ? round(($diff / $avg) * 100, 2) : ($monthly > 0 ? 100.0 : 0.0);
            // 편차 N% 이상 = |편차(%)| ≥ N (증가·감소 모두)
            if ($deviationMin > 0 && abs($pct) < $deviationMin) {
                continue;
            }
            $rows[] = [
                'agency_name'  => (string) ($shop['agency_name'] ?? ''),
                'shop_no'      => (string) ($shop['ds_shop_no'] ?? ''),
                'shop_name'    => (string) ($shop['ds_name'] ?? ''),
                'rep_name'     => (string) ($shop['ds_rep_name'] ?? ''),
                'prev_avg'     => (int) round($avg),
                'monthly_qty'  => (int) round($monthly),
                'avg_diff'     => (int) round($diff),
                'deviation_pct'=> $pct,
                'designated_at'=> (string) ($shop['ds_designated_at'] ?? ''),
            ];
        }

        usort($rows, static fn (array $a, array $b): int => strcmp((string) $a['shop_no'], (string) $b['shop_no']));

        return [
            'rows' => $rows,
            'meta' => [
                'shopCount'         => count($shops),
                'monthSalesShops'   => $monthSalesShops,
            ],
        ];
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function buildSeasonalTrend(
        int $lgIdx,
        int $baseYear,
        string $season,
        float $deviationMin,
        string $gugunCode,
        bool $queried
    ): array {
        if (! $queried) {
            return [];
        }

        $seasonKey = self::normalizeSeason($season);
        $seasonDef = self::seasonCatalog()[$seasonKey];
        $months = $seasonDef['months'];

        $saIdx = 0;
        $shops = $this->loadShops($lgIdx, $gugunCode, $saIdx);
        if ($shops === []) {
            return [];
        }

        $crossYear = (bool) ($seasonDef['cross_year'] ?? false);
        $currentByShop = $this->seasonalNetByShop($lgIdx, $baseYear, $months, $gugunCode, $saIdx, $crossYear);
        $prevByShop = $this->seasonalNetByShop($lgIdx, $baseYear - 1, $months, $gugunCode, $saIdx, $crossYear);

        $rows = [];
        foreach ($shops as $shop) {
            $sid = (int) ($shop['ds_idx'] ?? 0);
            $curr = (float) ($currentByShop[$sid] ?? 0.0);
            $prev = (float) ($prevByShop[$sid] ?? 0.0);
            $diff = $curr - $prev;
            $pct = $prev > 0 ? round(($diff / $prev) * 100, 2) : ($curr > 0 ? 100.0 : 0.0);
            if ($deviationMin > 0 && abs($pct) < $deviationMin) {
                continue;
            }
            $rows[] = [
                'agency_name'   => (string) ($shop['agency_name'] ?? ''),
                'shop_name'     => (string) ($shop['ds_name'] ?? ''),
                'shop_no'       => (string) ($shop['ds_shop_no'] ?? ''),
                'rep_name'      => (string) ($shop['ds_rep_name'] ?? ''),
                'prev_season_avg'=> (int) round($prev),
                'base_season_avg'=> (int) round($curr),
                'avg_diff'      => (int) round($diff),
                'deviation_pct' => $pct,
                'designated_at' => (string) ($shop['ds_designated_at'] ?? ''),
            ];
        }

        usort($rows, static fn (array $a, array $b): int => strcmp((string) $a['shop_no'], (string) $b['shop_no']));

        return $rows;
    }

    private function gugunLabel(int $lgIdx, string $code): string
    {
        static $cache = [];
        $key = $lgIdx . ':' . $code;
        if (isset($cache[$key])) {
            return $cache[$key];
        }
        $row = $this->db->table('code_detail cd')
            ->select('cd.cd_name')
            ->join('code_kind ck', 'ck.ck_idx = cd.cd_ck_idx', 'inner')
            ->where('ck.ck_code', 'G')
            ->where('cd.cd_lg_idx', $lgIdx)
            ->where('cd.cd_code', $code)
            ->get()
            ->getRowArray();
        $cache[$key] = trim((string) ($row['cd_name'] ?? $code));

        return $cache[$key];
    }

    private function resolveBagName(string $code): string
    {
        if (isset($this->bagNames[$code])) {
            return (string) $this->bagNames[$code];
        }
        if (ctype_digit($code) && isset($this->bagNames[(int) $code])) {
            return (string) $this->bagNames[(int) $code];
        }

        return $code;
    }

    private function loadBagNames(int $lgIdx): void
    {
        if ($this->bagNames !== []) {
            return;
        }
        $kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
        if (! $kindO) {
            return;
        }
        foreach (model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true, $lgIdx) as $d) {
            $code = trim((string) ($d->cd_code ?? ''));
            if ($code !== '') {
                $this->bagNames[$code] = trim((string) ($d->cd_name ?? $code));
            }
        }
    }

    /**
     * @param list<string> $codesFromAgg
     * @return list<string>
     */
    private function bagCodesForReport(int $lgIdx, array $codesFromAgg): array
    {
        $this->loadBagNames($lgIdx);
        $codes = array_keys($this->bagNames);
        if ($codesFromAgg !== []) {
            $merged = array_merge($codes, $codesFromAgg);
            $codes = [];
            foreach ($merged as $c) {
                $codes[] = (string) $c;
            }
            $codes = array_values(array_unique($codes));
            sort($codes, SORT_STRING);
        } else {
            $codes = array_map(static fn ($c): string => (string) $c, $codes);
        }

        return $codes;
    }

    /**
     * @return array<string, array<int, array<int, array{qty: float, amt: float}>>>
     */
    private function aggregateMonthlyByBag(
        int $lgIdx,
        int $fromYear,
        int $toYear,
        string $gugunCode,
        int $dsIdx
    ): array {
        $saleQty = $this->saleQtySql();
        $sql = "
            SELECT bs.bs_bag_code AS bag_code, YEAR(bs.bs_sale_date) AS y, MONTH(bs.bs_sale_date) AS m,
                   SUM({$saleQty}) AS sale_qty,
                   SUM(CASE WHEN bs.bs_type = 'sale' THEN bs.bs_amount ELSE 0 END) AS sale_amt
            FROM bag_sale bs
            INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
            WHERE bs.bs_lg_idx = ?
              AND YEAR(bs.bs_sale_date) BETWEEN ? AND ?
        ";
        $params = [$lgIdx, $fromYear, $toYear];
        if ($gugunCode !== '') {
            $sql .= ' AND ds.ds_gugun_code = ?';
            $params[] = $gugunCode;
        }
        if ($dsIdx > 0) {
            $sql .= ' AND bs.bs_ds_idx = ?';
            $params[] = $dsIdx;
        }
        $sql .= ' GROUP BY bs.bs_bag_code, YEAR(bs.bs_sale_date), MONTH(bs.bs_sale_date)';

        $agg = [];
        foreach ($this->db->query($sql, $params)->getResultArray() as $row) {
            $code = (string) ($row['bag_code'] ?? '');
            $y = (int) ($row['y'] ?? 0);
            $m = (int) ($row['m'] ?? 0);
            if ($code === '' || $y <= 0 || $m <= 0) {
                continue;
            }
            $agg[$code][$y][$m] = [
                'qty' => (float) ($row['sale_qty'] ?? 0),
                'amt' => (float) ($row['sale_amt'] ?? 0),
            ];
        }

        return $agg;
    }

    /**
     * @param array<string, array<int, array<int, array{qty: float, amt: float}>>> $agg
     * @param list<int> $months
     * @return array<string, mixed>
     */
    private function yoyBlock(
        string $code,
        string $name,
        string $section,
        array $agg,
        int $prevYear,
        int $year,
        array $months,
        bool $useAmount
    ): array {
        $key = $useAmount ? 'amt' : 'qty';
        $lines = [];
        foreach ([$prevYear => (string) $prevYear . '년', $year => (string) $year . '년', 0 => '증감'] as $y => $label) {
            $monthVals = [];
            $total = 0.0;
            foreach ($months as $mo) {
                $v = 0.0;
                if ($y === 0) {
                    $p = (float) ($agg[$code][$prevYear][$mo][$key] ?? 0);
                    $c = (float) ($agg[$code][$year][$mo][$key] ?? 0);
                    $v = $c - $p;
                } else {
                    $v = (float) ($agg[$code][$y][$mo][$key] ?? 0);
                }
                $monthVals[$mo] = (int) round($v);
                $total += $v;
            }
            $lines[] = ['label' => $label, 'months' => $monthVals, 'total' => (int) round($total)];
        }

        return [
            'bag_code' => $code,
            'bag_name' => $name,
            'section'  => $section,
            'lines'    => $lines,
        ];
    }

    /**
     * @return list<array<string, mixed>>
     */
    /**
     * @return array<string, string> 구·군코드 → 대행소명
     */
    private function agencyNameByGugun(int $lgIdx): array
    {
        $best = [];
        foreach ($this->db->query("
            SELECT TRIM(bo.bo_gugun_code) AS code, bo.bo_agency_idx AS sa_idx, COUNT(*) AS cnt
            FROM bag_order bo
            WHERE bo.bo_lg_idx = ?
              AND bo.bo_status = 'normal'
              AND bo.bo_agency_idx IS NOT NULL
              AND TRIM(bo.bo_gugun_code) != ''
            GROUP BY TRIM(bo.bo_gugun_code), bo.bo_agency_idx
        ", [$lgIdx])->getResultArray() as $row) {
            $code = (string) ($row['code'] ?? '');
            $cnt = (int) ($row['cnt'] ?? 0);
            if ($code === '') {
                continue;
            }
            if (! isset($best[$code]) || $cnt > $best[$code]['cnt']) {
                $best[$code] = ['cnt' => $cnt, 'sa_idx' => (int) ($row['sa_idx'] ?? 0)];
            }
        }

        $names = [];
        foreach ($best as $code => $info) {
            $sa = model(\App\Models\SalesAgencyModel::class)->find($info['sa_idx']);
            $names[$code] = $sa ? trim((string) ($sa->sa_name ?? '')) : '';
        }

        return $names;
    }

    private function loadShops(int $lgIdx, string $gugunCode, int $saIdx = 0): array
    {
        $hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');
        $agencyByGugun = $this->agencyNameByGugun($lgIdx);

        $sql = '
            SELECT ds.ds_idx, ds.ds_shop_no, ds.ds_name, ds.ds_rep_name, ds.ds_designated_at,
                   ds.ds_gugun_code';
        if ($hasDsSa) {
            $sql .= ', ds.ds_sa_idx';
        }
        $sql .= '
            FROM designated_shop ds
            WHERE ds.ds_lg_idx = ? AND ds.ds_state = 1
        ';
        $params = [$lgIdx];

        if ($gugunCode !== '') {
            $sql .= ' AND ds.ds_gugun_code = ?';
            $params[] = $gugunCode;
        }
        if ($saIdx > 0 && $hasDsSa) {
            $sql .= ' AND ds.ds_sa_idx = ?';
            $params[] = $saIdx;
        }
        $sql .= ' ORDER BY ds.ds_shop_no ASC, ds.ds_idx ASC';

        $rows = $this->db->query($sql, $params)->getResultArray();
        $saNames = [];
        if ($hasDsSa) {
            foreach (model(\App\Models\SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->findAll() as $sa) {
                $saNames[(int) ($sa->sa_idx ?? 0)] = trim((string) ($sa->sa_name ?? ''));
            }
        }

        foreach ($rows as &$row) {
            $name = '';
            if ($hasDsSa) {
                $saidx = (int) ($row['ds_sa_idx'] ?? 0);
                $name = $saNames[$saidx] ?? '';
            }
            if ($name === '') {
                $code = trim((string) ($row['ds_gugun_code'] ?? ''));
                $name = $agencyByGugun[$code] ?? '';
            }
            $row['agency_name'] = $name;
        }
        unset($row);

        return $rows;
    }

    /**
     * @return array<int, float>
     */
    private function monthlyNetByShop(int $lgIdx, int $year, int $month, string $gugunCode, int $saIdx = 0): array
    {
        $hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');
        $saleQty = $this->saleQtySql();
        $sql = "
            SELECT bs.bs_ds_idx AS ds_idx,
                   SUM({$saleQty}) AS sale_qty
            FROM bag_sale bs
            INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
            WHERE bs.bs_lg_idx = ? AND YEAR(bs.bs_sale_date) = ? AND MONTH(bs.bs_sale_date) = ?
              AND bs.bs_ds_idx IS NOT NULL
        ";
        $params = [$lgIdx, $year, $month];
        if ($gugunCode !== '') {
            $sql .= ' AND ds.ds_gugun_code = ?';
            $params[] = $gugunCode;
        }
        if ($saIdx > 0 && $hasDsSa) {
            $sql .= ' AND ds.ds_sa_idx = ?';
            $params[] = $saIdx;
        }
        $sql .= ' GROUP BY bs.bs_ds_idx';

        $map = [];
        foreach ($this->db->query($sql, $params)->getResultArray() as $row) {
            $map[(int) ($row['ds_idx'] ?? 0)] = (float) ($row['sale_qty'] ?? 0);
        }

        return $map;
    }

    /**
     * @return array<int, float>
     */
    private function averageNetByShop(
        int $lgIdx,
        int $year,
        string $gugunCode,
        string $trendBasis,
        int $refMonth,
        int $saIdx = 0
    ): array {
        if ($trendBasis === 'month') {
            return $this->monthlyNetByShop($lgIdx, $year, $refMonth, $gugunCode, $saIdx);
        }

        $hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');
        $saleQty = $this->saleQtySql();
        $sql = "
            SELECT bs.bs_ds_idx AS ds_idx,
                   SUM({$saleQty}) / 12 AS avg_qty
            FROM bag_sale bs
            INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
            WHERE bs.bs_lg_idx = ? AND YEAR(bs.bs_sale_date) = ?
              AND bs.bs_ds_idx IS NOT NULL
        ";
        $params = [$lgIdx, $year];
        if ($gugunCode !== '') {
            $sql .= ' AND ds.ds_gugun_code = ?';
            $params[] = $gugunCode;
        }
        if ($saIdx > 0 && $hasDsSa) {
            $sql .= ' AND ds.ds_sa_idx = ?';
            $params[] = $saIdx;
        }
        $sql .= ' GROUP BY bs.bs_ds_idx';

        $map = [];
        foreach ($this->db->query($sql, $params)->getResultArray() as $row) {
            $map[(int) ($row['ds_idx'] ?? 0)] = (float) ($row['avg_qty'] ?? 0);
        }

        return $map;
    }

    /**
     * @param list<int> $months
     * @return array<int, float>
     */
    private function seasonalNetByShop(
        int $lgIdx,
        int $year,
        array $months,
        string $gugunCode,
        int $saIdx = 0,
        bool $crossYearWinter = false
    ): array {
        if ($months === []) {
            return [];
        }

        $divisor = count($months);
        $saleQty = $this->saleQtySql();
        $hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');

        if ($crossYearWinter) {
            $sql = "
                SELECT bs.bs_ds_idx AS ds_idx,
                       SUM({$saleQty}) / ? AS avg_qty
                FROM bag_sale bs
                INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
                WHERE bs.bs_lg_idx = ?
                  AND bs.bs_ds_idx IS NOT NULL
                  AND (
                    (YEAR(bs.bs_sale_date) = ? AND MONTH(bs.bs_sale_date) = 12)
                    OR (YEAR(bs.bs_sale_date) = ? AND MONTH(bs.bs_sale_date) IN (1, 2))
                  )
            ";
            $params = [$divisor, $lgIdx, $year - 1, $year];
        } else {
            $placeholders = implode(',', array_fill(0, count($months), '?'));
            $sql = "
                SELECT bs.bs_ds_idx AS ds_idx,
                       SUM({$saleQty}) / ? AS avg_qty
                FROM bag_sale bs
                INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
                WHERE bs.bs_lg_idx = ? AND YEAR(bs.bs_sale_date) = ?
                  AND MONTH(bs.bs_sale_date) IN ({$placeholders})
                  AND bs.bs_ds_idx IS NOT NULL
            ";
            $params = array_merge([$divisor], [$lgIdx, $year], $months);
        }

        if ($gugunCode !== '') {
            $sql .= ' AND ds.ds_gugun_code = ?';
            $params[] = $gugunCode;
        }
        if ($saIdx > 0 && $hasDsSa) {
            $sql .= ' AND ds.ds_sa_idx = ?';
            $params[] = $saIdx;
        }
        $sql .= ' GROUP BY bs.bs_ds_idx';

        $map = [];
        foreach ($this->db->query($sql, $params)->getResultArray() as $row) {
            $map[(int) ($row['ds_idx'] ?? 0)] = (float) ($row['avg_qty'] ?? 0);
        }

        return $map;
    }
}
