# -*- coding: utf-8 -*-
"""
将原 UFO 站点商品目录灌入星阙，且只写 club_id=ufo。

默认 dry-run（只打印计划，不写库）。真正写入必须加 --apply。
默认按全局类型名「三角洲」**全名精确匹配**找 id（不含这三字的一律不要）。
找到唯一一条后：
  1) ClubShangpinLeixingConfig 给 club=ufo 上架该类型
  2) ShangpinZhuanqu / Shangpin 的 leixing_id 挂这个全局类型 id，且 club_id=ufo

服务器上（复制整段）:

  cd /opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.443/django
  git pull origin main
  ./venv/bin/python manage.py import_ufo_catalog
  ./venv/bin/python manage.py import_ufo_catalog --apply
  ./venv/bin/python manage.py import_ufo_catalog --apply --with-banners
"""
from __future__ import annotations

import json
import mimetypes
import os
import re
import ssl
import uuid
from decimal import Decimal, InvalidOperation
from io import BytesIO
from pathlib import Path
from urllib.request import Request, urlopen

from django.core.management.base import BaseCommand, CommandError
from django.db import transaction

TARGET_CLUB = 'ufo'
DEFAULT_LEIXING_NAME = '三角洲'  # 全名精确匹配，不模糊
DEFAULT_CATALOG = Path(__file__).resolve().parents[2] / 'data' / 'ufo_catalog.json'


class Command(BaseCommand):
    help = '导入 UFO 商品/专区到 club_id=ufo（默认 dry-run；类型名默认精确匹配「三角洲」）'

    def add_arguments(self, parser):
        parser.add_argument('--apply', action='store_true', help='真正写库（仅 ufo）')
        parser.add_argument('--catalog', type=str, default='', help='catalog.json 路径')
        parser.add_argument(
            '--list-leixing',
            action='store_true',
            help='只列出全局商品类型 id/名称',
        )
        parser.add_argument(
            '--leixing-id',
            type=int,
            default=0,
            help='可选：直接指定全局类型主键；不传则按 --leixing-name 精确匹配',
        )
        parser.add_argument(
            '--leixing-name',
            type=str,
            default=DEFAULT_LEIXING_NAME,
            help='全局 ShangpinLeixing.jieshao 全名精确匹配，默认：三角洲',
        )
        parser.add_argument('--with-banners', action='store_true', help='同时导入点单主轮播+第二横幅')
        parser.add_argument('--skip-oss', action='store_true', help='不上传 OSS，保留原 URL（应急）')
        parser.add_argument('--club-id', type=str, default=TARGET_CLUB, help='仅允许 ufo')

    def handle(self, *args, **options):
        from products.models import ShangpinLeixing

        if options.get('list_leixing'):
            self.stdout.write('全局商品类型清单（ShangpinLeixing）：')
            for lx in ShangpinLeixing.query.order_by('id'):
                mark = '  <<-- 精确名=三角洲' if (lx.jieshao or '') == DEFAULT_LEIXING_NAME else ''
                self.stdout.write(
                    f'  id={lx.id}  name={lx.jieshao!r}  '
                    f'shenhezhuangtai={lx.shenhezhuangtai}  paixu={lx.paixu}{mark}'
                )
            self.stdout.write(self.style.NOTICE(
                '默认会精确匹配 name==「三角洲」；也可 --leixing-id=... 覆盖。\n'
                '俱乐部上架表 ClubShangpinLeixingConfig 会在 --apply 时给 ufo 自动开启该类型。'
            ))
            return

        club_id = (options.get('club_id') or '').strip()
        if club_id != TARGET_CLUB:
            raise CommandError(
                f'拒绝执行：只允许 club_id={TARGET_CLUB}，你传的是 {club_id!r}'
            )

        catalog_path = Path(options['catalog']).expanduser() if options['catalog'] else DEFAULT_CATALOG
        if not catalog_path.is_file():
            raise CommandError(f'找不到目录文件: {catalog_path}')

        data = json.loads(catalog_path.read_text(encoding='utf-8'))
        products = data.get('products') or []
        zones = data.get('zones') or []
        banners = data.get('banners') or []
        banners2 = data.get('banners2') or []
        apply = bool(options['apply'])
        with_banners = bool(options['with_banners'])
        skip_oss = bool(options['skip_oss'])

        from jituan.models import Club, ClubShangpinLeixingConfig
        from products.models import Shangpin, ShangpinZhuanqu

        if not Club.query.filter(club_id=TARGET_CLUB).exists():
            raise CommandError(f'俱乐部 {TARGET_CLUB} 不存在，请先 create_club ufo')

        leixing = self._resolve_leixing(
            ShangpinLeixing,
            leixing_id=int(options.get('leixing_id') or 0),
            leixing_name=(options.get('leixing_name') or '').strip(),
        )

        self.stdout.write(self.style.NOTICE(
            f'目标俱乐部={TARGET_CLUB} 类型={leixing.jieshao!r}(id={leixing.id}) '
            f'shenhezhuangtai={leixing.shenhezhuangtai} '
            f'专区={len(zones)} 商品={len(products)} banners={len(banners)}+{len(banners2)} '
            f'mode={"APPLY" if apply else "DRY-RUN"}'
        ))
        if leixing.shenhezhuangtai == 2:
            raise CommandError(
                f'类型 id={leixing.id}「{leixing.jieshao}」是审核专用，拒绝挂载'
            )
        expect_name = (options.get('leixing_name') or DEFAULT_LEIXING_NAME).strip()
        if (leixing.jieshao or '') != expect_name and not int(options.get('leixing_id') or 0):
            raise CommandError(
                f'安全拦截：解析到的类型名是「{leixing.jieshao}」，'
                f'与要求的精确名「{expect_name}」不一致。'
            )

        self.stdout.write(
            f'将写入：ClubShangpinLeixingConfig(club=ufo, leixing_id={leixing.id}, is_enabled=True)；'
            f'专区/商品 leixing_id={leixing.id}, club_id=ufo'
        )

        for z in zones:
            self.stdout.write(f'  专区: {z.get("name")} goods={len(z.get("goods_ids") or [])}')
        for p in products[:5]:
            self.stdout.write(
                f'  商品样例: [{p.get("source_id")}] {p.get("title")} ¥{p.get("price")} → {p.get("zone_name")}'
            )
        if len(products) > 5:
            self.stdout.write(f'  ... 其余 {len(products) - 5} 个商品省略')

        if not apply:
            self.stdout.write(self.style.WARNING(
                '当前 dry-run，未写库。确认无误后：\n'
                '  ./venv/bin/python manage.py import_ufo_catalog --apply\n'
                '  ./venv/bin/python manage.py import_ufo_catalog --apply --with-banners'
            ))
            return

        stats = {
            'leixing_enabled': 0,
            'zones_created': 0,
            'zones_updated': 0,
            'products_created': 0,
            'products_updated': 0,
            'banners': 0,
            'oss_ok': 0,
            'oss_fail': 0,
        }

        def to_oss(url: str, prefix: str) -> str:
            """下载原图 → 上传到本站 OSS → 返回相对路径（与后台 AddProduct 一致）。"""
            if not url:
                return ''
            if skip_oss:
                # 应急：允许写完整 URL；正式导入不要加 --skip-oss
                return url
            if not str(url).startswith('http'):
                return url
            path = self._upload_remote(url, prefix)
            if not path:
                stats['oss_fail'] += 1
                raise CommandError(f'OSS 上传失败，已中止以免库里写入外站链接: {url}')
            stats['oss_ok'] += 1
            return path

        with transaction.atomic():
            cfg, created = ClubShangpinLeixingConfig.query.get_or_create(
                club_id=TARGET_CLUB,
                leixing_id=leixing.id,
                defaults={
                    'is_enabled': True,
                    'paixu': int(leixing.paixu or 0),
                    'tupian_url': '',
                },
            )
            if not cfg.is_enabled:
                cfg.is_enabled = True
                cfg.save(update_fields=['is_enabled', 'UpdateTime'])
                stats['leixing_enabled'] += 1
            elif created:
                stats['leixing_enabled'] += 1

            zone_id_map = {}
            for idx, z in enumerate(zones):
                name = (z.get('name') or '').strip()
                if not name:
                    continue
                src_id = int(z.get('source_id') or 0)
                thumb = to_oss(z.get('thumb_url') or '', 'shangpin/zhuanqu')
                row = ShangpinZhuanqu.query.filter(
                    club_id=TARGET_CLUB,
                    mingzi=name,
                    leixing_id=leixing.id,
                    dianpu__isnull=True,
                ).first()
                if row:
                    row.tupian_url = thumb or row.tupian_url
                    row.shenhezhuangtai = 1
                    row.paixu = max(len(zones) - idx, 0)
                    row.save(update_fields=['tupian_url', 'shenhezhuangtai', 'paixu', 'UpdateTime'])
                    stats['zones_updated'] += 1
                else:
                    row = ShangpinZhuanqu.query.create(
                        mingzi=name,
                        leixing_id=leixing.id,
                        club_id=TARGET_CLUB,
                        tupian_url=thumb or '',
                        shenhezhuangtai=1,
                        paixu=max(len(zones) - idx, 0),
                        dianpu=None,
                        dianpu_leixing=None,
                    )
                    stats['zones_created'] += 1
                if src_id:
                    zone_id_map[src_id] = row.id
                zone_id_map[name] = row.id

            for p in products:
                title = (p.get('title') or '').strip()
                if not title:
                    continue
                zone_name = (p.get('zone_name') or p.get('target_zhuanqu_name') or '').strip()
                cat2 = int(p.get('cat2_id') or 0)
                zhuanqu_id = zone_id_map.get(cat2) or zone_id_map.get(zone_name)
                if not zhuanqu_id:
                    self.stderr.write(f'跳过无专区商品: {title} zone={zone_name}')
                    continue

                price = self._decimal(p.get('price'))
                intro = (p.get('intro') or '').strip()
                xuzhi = (p.get('buy_tips_text') or '').strip()
                if p.get('rise_reason'):
                    xuzhi = ((xuzhi + '\n\n') if xuzhi else '') + f"加价说明：{p.get('rise_reason')}"

                pics = list(p.get('pics_url') or [])
                thumb = p.get('thumb_url') or (pics[0] if pics else '')
                tupian = to_oss(thumb, 'shangpin/shangpintupian')
                rule_imgs = list(p.get('rule_images_url') or [])
                guize = to_oss(rule_imgs[0], 'shangpin/guize') if rule_imgs else ''

                weigh = int(p.get('weigh') or p.get('home_weigh') or 0)
                on_shelf = int(p.get('status') or 1) == 1
                is_raffle = int(p.get('raffle_id') or 0) > 0

                row = Shangpin.query.filter(
                    club_id=TARGET_CLUB,
                    biaoqian=title,
                    zhuanqu_id=zhuanqu_id,
                    dianpu__isnull=True,
                ).first()

                fields = dict(
                    biaoqian=title,
                    jiage=price,
                    kucun=99999,
                    leixing_id=leixing.id,
                    zhuanqu_id=zhuanqu_id,
                    jieshao=intro,
                    xiadan_xuzhi=xuzhi[:5000] if xuzhi else '',
                    guize_tupian=guize or '',
                    tupian_url=tupian or '',
                    yaoqiuleixing=leixing.yaoqiuleixing,
                    huiyuan_id=leixing.huiyuan_id,
                    yongjin=leixing.yongjin,
                    paixu=weigh,
                    shenhezhuangtai=1,
                    shangjia_zhuangtai=on_shelf,
                    club_id=TARGET_CLUB,
                    shi_zhuanpan=bool(is_raffle),
                    dianpu=None,
                    dianpu_leixing=None,
                )

                if row:
                    for k, v in fields.items():
                        setattr(row, k, v)
                    row.save()
                    stats['products_updated'] += 1
                else:
                    Shangpin.query.create(
                        zhenshi_xiaoliang=0,
                        duiwai_xiaoliang=0,
                        **fields,
                    )
                    stats['products_created'] += 1

            if with_banners:
                from config.models import Lunbo
                from jituan.services.display_config import (
                    LUNBO_PAGE_ACCEPT_BANNER2,
                    LUNBO_PAGE_ACCEPT_ORDER,
                    lunbo_image_type_for_page,
                )

                def upsert_lunbo(page_key, items):
                    image_type = lunbo_image_type_for_page(page_key)
                    Lunbo.query.filter(
                        club_id=TARGET_CLUB,
                        page_key=page_key,
                        ImageType=image_type,
                    ).delete()
                    for b in items:
                        url = to_oss(b.get('url') or '', 'a_long/lunbo')
                        if not url:
                            continue
                        Lunbo.query.create(
                            club_id=TARGET_CLUB,
                            page_key=page_key,
                            ImageURL=url,
                            ImageType=image_type,
                        )
                        stats['banners'] += 1

                upsert_lunbo(LUNBO_PAGE_ACCEPT_ORDER, banners)
                upsert_lunbo(LUNBO_PAGE_ACCEPT_BANNER2, banners2)

        self.stdout.write(self.style.SUCCESS(
            '完成（仅 club=ufo）: '
            f"类型上架={stats['leixing_enabled']} "
            f"专区+={stats['zones_created']}/改={stats['zones_updated']} "
            f"商品+={stats['products_created']}/改={stats['products_updated']} "
            f"轮播={stats['banners']} "
            f"OSS成功={stats['oss_ok']} 失败={stats['oss_fail']}"
        ))

    def _resolve_leixing(self, ShangpinLeixing, leixing_id: int, leixing_name: str):
        """优先 id；否则 jieshao 全名精确匹配且必须唯一。不做 icontains。"""
        if leixing_id:
            try:
                lx = ShangpinLeixing.query.get(id=leixing_id)
            except ShangpinLeixing.DoesNotExist as exc:
                raise CommandError(f'类型 id={leixing_id} 不存在') from exc
            return lx

        name = (leixing_name or DEFAULT_LEIXING_NAME).strip() or DEFAULT_LEIXING_NAME
        rows = list(
            ShangpinLeixing.query.filter(jieshao=name)
            .exclude(shenhezhuangtai=2)
            .order_by('id')
        )
        if not rows:
            near = list(
                ShangpinLeixing.query.filter(jieshao__icontains=name).order_by('id')[:20]
            )
            hint = '\n'.join(
                f'  id={x.id} name={x.jieshao!r} shenhezhuangtai={x.shenhezhuangtai}'
                for x in near
            ) or '  （无近似）'
            raise CommandError(
                f'找不到 jieshao 精确等于「{name}」且非审核专用的类型。库中近似：\n{hint}\n'
                '请先在后台把类型名改成正好「三角洲」，或 --leixing-id=... 指定。'
            )
        if len(rows) > 1:
            detail = '\n'.join(
                f'  id={x.id} name={x.jieshao!r} shenhezhuangtai={x.shenhezhuangtai}'
                for x in rows
            )
            raise CommandError(
                f'「{name}」精确匹配到多条，拒绝自动选，避免挂错：\n{detail}'
            )
        return rows[0]

    @staticmethod
    def _decimal(v) -> Decimal:
        try:
            return Decimal(str(v if v is not None else '0'))
        except (InvalidOperation, TypeError, ValueError):
            return Decimal('0')

    @staticmethod
    def _upload_remote(url: str, prefix: str) -> str:
        """返回 OSS 相对 key；失败返回空串。与后台一致：DB 只存相对路径。"""
        from datetime import datetime

        from utils.oss_utils import upload_to_oss

        try:
            ctx = ssl.create_default_context()
            req = Request(url, headers={'User-Agent': 'UFO-Import/1.0'})
            with urlopen(req, timeout=60, context=ctx) as resp:
                raw = resp.read()
                ctype = resp.headers.get('Content-Type') or ''
        except Exception:  # noqa: BLE001
            return ''

        ext = mimetypes.guess_extension((ctype or '').split(';')[0].strip()) or ''
        if not ext or ext == '.jpe':
            m = re.search(r'\.(jpg|jpeg|png|gif|webp|bmp)(?:\?|$)', url, re.I)
            ext = ('.' + m.group(1).lower()) if m else '.jpg'
        if ext == '.jpeg':
            ext = '.jpg'

        ts = datetime.now().strftime('%Y%m%d%H%M%S')
        rel = f"{prefix.strip('/')}/{ts}_{uuid.uuid4().hex[:8]}{ext}"
        bio = BytesIO(raw)
        bio.name = os.path.basename(rel)
        ok = upload_to_oss(bio, rel)
        if not ok:
            return ''
        return rel  # 关键：只返回相对路径，不返回完整 CDN URL
