# -*- coding: utf-8 -*-
"""
将 UFO 俱乐部已上架的「三角洲」公共商品目录复制到龙先生俱乐部（只增不减）。

涉及表（只写目标俱乐部，不改源、不删任何行）：
  - club_shangpin_leixing_config  ClubShangpinLeixingConfig  上架类型
  - shangpin_zhuanqu              ShangpinZhuanqu             公共专区
  - shangpin                      Shangpin                    公共商品
  - OSS                           copy_in_oss                图物理复制到新相对路径

不碰：
  - shangpin_leixing（全局类型只复用，不新建）
  - 店铺商品（dianpu 非空）
  - 订单/统计/轮播/转盘活动表
  - UFO 及其它俱乐部任何行

用法（服务器）:

  cd /path/to/django
  ./venv/bin/python manage.py copy_ufo_sanjiaozhou_catalog --to-club=lxs
  # 确认清单后：
  ./venv/bin/python manage.py copy_ufo_sanjiaozhou_catalog --to-club=lxs --apply --i-confirm-to-club=lxs

注意：龙先生 club_id 可能是 lxs 或 lsx，脚本不猜测，必须显式传 --to-club，
且 Club.name 必须包含「龙先生」。
"""
from __future__ import annotations

import time
import uuid

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

DEFAULT_FROM_CLUB = 'ufo'
DEFAULT_LEIXING_NAME = '三角洲'
NAME_HINT = '龙先生'


class Command(BaseCommand):
    help = (
        '复制 UFO「三角洲」公共上架商品目录到龙先生（默认 dry-run；'
        '只 insert / 启用类型，不删不改源）'
    )

    def add_arguments(self, parser):
        parser.add_argument(
            '--from-club',
            type=str,
            default=DEFAULT_FROM_CLUB,
            help=f'源俱乐部，默认 {DEFAULT_FROM_CLUB}',
        )
        parser.add_argument(
            '--to-club',
            type=str,
            default='',
            help='目标俱乐部 club_id（必须显式指定，如 lxs；--list-clubs 时可省略）',
        )
        parser.add_argument(
            '--leixing-name',
            type=str,
            default=DEFAULT_LEIXING_NAME,
            help=f'全局类型名全名精确匹配，默认「{DEFAULT_LEIXING_NAME}」',
        )
        parser.add_argument(
            '--leixing-id',
            type=int,
            default=0,
            help='可选：直接指定全局 ShangpinLeixing.id（仍校验名称）',
        )
        parser.add_argument(
            '--apply',
            action='store_true',
            help='真正写库 + OSS 复制（默认仅 dry-run）',
        )
        parser.add_argument(
            '--i-confirm-to-club',
            type=str,
            default='',
            help='与 --to-club 完全一致时才允许 --apply',
        )
        parser.add_argument(
            '--list-clubs',
            action='store_true',
            help='只列出 Club 表（含名称含「龙先生」的候选），不复制',
        )

    def handle(self, *args, **options):
        from jituan.models import Club, ClubShangpinLeixingConfig
        from products.models import Shangpin, ShangpinLeixing, ShangpinZhuanqu
        from utils.oss_utils import copy_in_oss

        if options.get('list_clubs'):
            self._list_clubs(Club)
            return

        from_club = (options.get('from_club') or DEFAULT_FROM_CLUB).strip()
        to_club = (options.get('to_club') or '').strip()
        leixing_name = (options.get('leixing_name') or DEFAULT_LEIXING_NAME).strip()
        apply = bool(options.get('apply'))
        confirm = (options.get('i_confirm_to_club') or '').strip()

        if not to_club:
            raise CommandError(
                '必须传 --to-club=...（龙先生当前库为 lxs）。'
                '仅查看俱乐部列表时用：python manage.py copy_ufo_sanjiaozhou_catalog --list-clubs'
            )

        self.stdout.write(self.style.NOTICE('—— Club 表候选（名称含「龙先生」）——'))
        self._list_clubs(Club, only_hint=True)

        src_club = Club.query.filter(club_id=from_club).first()
        if not src_club:
            raise CommandError(f'源俱乐部不存在: club_id={from_club!r}')

        dst_club = Club.query.filter(club_id=to_club).first()
        if not dst_club:
            raise CommandError(
                f'目标俱乐部不存在: club_id={to_club!r}。'
                f'请先 --list-clubs 核对（常见：lxs 与 lsx 写反）。'
            )
        if NAME_HINT not in (dst_club.name or ''):
            raise CommandError(
                f'安全拦截：目标 club_id={to_club!r} 的 name={dst_club.name!r} '
                f'不含「{NAME_HINT}」，拒绝执行。'
            )
        if from_club == to_club:
            raise CommandError('源与目标 club_id 不能相同')

        if apply:
            if confirm != to_club:
                raise CommandError(
                    '安全拦截：--apply 必须同时传 --i-confirm-to-club，且与 --to-club 完全一致。\n'
                    f'  例：--to-club={to_club} --apply --i-confirm-to-club={to_club}'
                )

        leixing = self._resolve_leixing(
            ShangpinLeixing,
            leixing_id=int(options.get('leixing_id') or 0),
            leixing_name=leixing_name,
        )
        if leixing.shenhezhuangtai == 2:
            raise CommandError(
                f'类型 id={leixing.id}「{leixing.jieshao}」是审核专用，拒绝挂载'
            )

        # 源：公共专区 + 上架商品（dianpu 为空 = 俱乐部公共目录）
        src_zones = list(
            ShangpinZhuanqu.query.filter(
                club_id=from_club,
                leixing_id=leixing.id,
                shenhezhuangtai=1,
                dianpu__isnull=True,
            ).order_by('paixu', 'id')
        )

        zone_ids = [z.id for z in src_zones]
        src_products = []
        if zone_ids:
            src_products = list(
                Shangpin.query.filter(
                    club_id=from_club,
                    leixing_id=leixing.id,
                    zhuanqu_id__in=zone_ids,
                    shenhezhuangtai=1,
                    shangjia_zhuangtai=True,
                    dianpu__isnull=True,
                ).order_by('paixu', 'id')
            )

        # 计划：Config
        cfg_existing = ClubShangpinLeixingConfig.query.filter(
            club_id=to_club, leixing_id=leixing.id,
        ).first()
        if cfg_existing is None:
            cfg_action = 'CREATE_ENABLE'
        elif not cfg_existing.is_enabled:
            cfg_action = 'ENABLE_EXISTING'
        else:
            cfg_action = 'SKIP_ALREADY_ENABLED'

        # 计划：专区
        zone_plans = []
        for z in src_zones:
            exist = ShangpinZhuanqu.query.filter(
                club_id=to_club,
                leixing_id=leixing.id,
                mingzi=z.mingzi,
                dianpu__isnull=True,
            ).first()
            if exist:
                zone_plans.append({
                    'source': z,
                    'action': 'SKIP',
                    'target_id': exist.id,
                    'new_tupian_url': exist.tupian_url or '',
                })
            else:
                zone_plans.append({
                    'source': z,
                    'action': 'CREATE',
                    'target_id': None,
                    'new_tupian_url': None,
                })

        zone_by_src_id = {p['source'].id: p for p in zone_plans}

        product_plans = []
        for p in src_products:
            zp = zone_by_src_id.get(p.zhuanqu_id)
            if not zp:
                product_plans.append({
                    'source': p,
                    'action': 'SKIP_NO_ZONE',
                    'reason': f'zhuanqu_id={p.zhuanqu_id} 不在本批三角洲专区',
                })
                continue
            planned_zq_id = zp.get('target_id')
            if planned_zq_id:
                exist_p = Shangpin.query.filter(
                    club_id=to_club,
                    zhuanqu_id=planned_zq_id,
                    biaoqian=p.biaoqian,
                    dianpu__isnull=True,
                ).first()
                if exist_p:
                    product_plans.append({
                        'source': p,
                        'action': 'SKIP',
                        'zone_plan': zp,
                        'target_zhuanqu_id': planned_zq_id,
                    })
                    continue
            product_plans.append({
                'source': p,
                'action': 'CREATE',
                'zone_plan': zp,
                'target_zhuanqu_id': planned_zq_id,
            })

        # —— 打印确认清单 ——
        mode = 'APPLY' if apply else 'DRY-RUN'
        self.stdout.write(self.style.NOTICE(
            f'\n===== {mode} =====\n'
            f'源: {from_club} ({src_club.name}) → 目标: {to_club} ({dst_club.name})\n'
            f'类型: jieshao={leixing.jieshao!r} id={leixing.id}（全局复用，不新建）\n'
            f'ClubShangpinLeixingConfig: {cfg_action}\n'
            f'源专区={len(src_zones)} 源上架商品={len(src_products)}\n'
        ))

        self.stdout.write('—— 专区计划 ——')
        for i, zp in enumerate(zone_plans, 1):
            z = zp['source']
            self.stdout.write(
                f'  [{i}] {zp["action"]}  src_id={z.id}  mingzi={z.mingzi!r}  '
                f'paixu={z.paixu}  tupian={z.tupian_url or "-"}'
                + (f'  → existing_id={zp["target_id"]}' if zp['target_id'] else '')
            )

        self.stdout.write('—— 商品计划 ——')
        for i, pp in enumerate(product_plans, 1):
            p = pp['source']
            zname = ''
            zp = pp.get('zone_plan')
            if zp:
                zname = zp['source'].mingzi
            self.stdout.write(
                f'  [{i}] {pp["action"]}  src_id={p.id}  {p.biaoqian!r}  '
                f'¥{p.jiage}  zone={zname!r}  '
                f'tupian={p.tupian_url or "-"}  guize={p.guize_tupian or "-"}'
                + (f'  reason={pp.get("reason")}' if pp.get('reason') else '')
            )

        stats_preview = {
            'cfg': cfg_action,
            'zones_create': sum(1 for x in zone_plans if x['action'] == 'CREATE'),
            'zones_skip': sum(1 for x in zone_plans if x['action'] == 'SKIP'),
            'products_create': sum(1 for x in product_plans if x['action'] == 'CREATE'),
            'products_skip': sum(1 for x in product_plans if x['action'] == 'SKIP'),
            'products_skip_no_zone': sum(1 for x in product_plans if x['action'] == 'SKIP_NO_ZONE'),
        }
        self.stdout.write(self.style.NOTICE(f'\n统计预览: {stats_preview}'))

        if not apply:
            self.stdout.write(self.style.WARNING(
                '\n以上仅计划，未写库、未拷 OSS。\n'
                '确认无误后执行：\n'
                f'  python manage.py copy_ufo_sanjiaozhou_catalog '
                f'--to-club={to_club} --apply --i-confirm-to-club={to_club}\n'
            ))
            return

        # —— APPLY：先 OSS，再 DB 事务 ——
        def dest_key(category: str, src_url: str) -> str:
            ext = 'png'
            raw = (src_url or '').split('?', 1)[0]
            if '.' in raw.rsplit('/', 1)[-1]:
                ext = raw.rsplit('.', 1)[-1].lower()[:8] or 'png'
            ts = int(time.time() * 1000)
            rand = uuid.uuid4().hex[:8]
            return f'club/{to_club}/catalog/{category}/{ts}_{rand}.{ext}'

        def copy_img(src_url: str, category: str) -> str:
            if not src_url:
                return ''
            # 已是相对路径或 URL：物理复制到新 key
            key = dest_key(category, src_url)
            if not copy_in_oss(src_url, key):
                raise CommandError(f'OSS 复制失败，已中止（未写库）: {src_url} → {key}')
            return key

        # 预复制专区/商品图（仅 CREATE）
        oss_ok = 0
        for zp in zone_plans:
            if zp['action'] != 'CREATE':
                continue
            z = zp['source']
            if z.tupian_url:
                zp['new_tupian_url'] = copy_img(z.tupian_url, 'zhuanqu')
                oss_ok += 1
            else:
                zp['new_tupian_url'] = ''

        for pp in product_plans:
            if pp['action'] != 'CREATE':
                continue
            p = pp['source']
            pp['new_tupian_url'] = copy_img(p.tupian_url, 'shangpin') if p.tupian_url else ''
            if p.tupian_url:
                oss_ok += 1
            if p.guize_tupian:
                pp['new_guize_tupian'] = copy_img(p.guize_tupian, 'guize')
                oss_ok += 1
            else:
                pp['new_guize_tupian'] = ''

        stats = {
            'cfg_created': 0,
            'cfg_enabled': 0,
            'zones_created': 0,
            'zones_skipped': 0,
            'products_created': 0,
            'products_skipped': 0,
            'products_skipped_no_zone': 0,
            'oss_copied': oss_ok,
        }

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

            # 专区 insert + 映射
            zone_id_map = {}  # src_id -> dst_id
            for zp in zone_plans:
                z = zp['source']
                if zp['action'] == 'SKIP':
                    zone_id_map[z.id] = zp['target_id']
                    stats['zones_skipped'] += 1
                    continue
                # 事务内再查一次，防并发重复
                exist = ShangpinZhuanqu.query.filter(
                    club_id=to_club,
                    leixing_id=leixing.id,
                    mingzi=z.mingzi,
                    dianpu__isnull=True,
                ).first()
                if exist:
                    zone_id_map[z.id] = exist.id
                    stats['zones_skipped'] += 1
                    continue
                row = ShangpinZhuanqu.query.create(
                    mingzi=z.mingzi,
                    leixing_id=leixing.id,
                    club_id=to_club,
                    tupian_url=zp.get('new_tupian_url') or '',
                    shenhezhuangtai=1,
                    paixu=int(z.paixu or 0),
                    shangjia_zhuangtai=bool(getattr(z, 'shangjia_zhuangtai', True)),
                    fengjin_zhuangtai=bool(getattr(z, 'fengjin_zhuangtai', True)),
                    dianpu=None,
                    dianpu_leixing=None,
                )
                zone_id_map[z.id] = row.id
                zp['target_id'] = row.id
                stats['zones_created'] += 1

            for pp in product_plans:
                p = pp['source']
                if pp['action'] == 'SKIP_NO_ZONE':
                    stats['products_skipped_no_zone'] += 1
                    continue
                if pp['action'] == 'SKIP':
                    stats['products_skipped'] += 1
                    continue

                dst_zq = zone_id_map.get(p.zhuanqu_id)
                if not dst_zq:
                    raise CommandError(
                        f'商品「{p.biaoqian}」src_id={p.id} 无法映射 zhuanqu_id={p.zhuanqu_id}'
                    )

                exist_p = Shangpin.query.filter(
                    club_id=to_club,
                    zhuanqu_id=dst_zq,
                    biaoqian=p.biaoqian,
                    dianpu__isnull=True,
                ).first()
                if exist_p:
                    stats['products_skipped'] += 1
                    continue

                Shangpin.query.create(
                    biaoqian=p.biaoqian,
                    jiage=p.jiage,
                    kucun=p.kucun,
                    leixing_id=leixing.id,
                    zhuanqu_id=dst_zq,
                    zhenshi_xiaoliang=0,
                    duiwai_xiaoliang=0,
                    jieshao=p.jieshao,
                    xiadan_xuzhi=p.xiadan_xuzhi,
                    guize_tupian=pp.get('new_guize_tupian') or '',
                    yaoqiuleixing=p.yaoqiuleixing,
                    huiyuan_id=p.huiyuan_id,
                    yongjin=p.yongjin,
                    kaioi_ewai_dashou_fencheng=bool(p.kaioi_ewai_dashou_fencheng),
                    ewai_dashou_fencheng=p.ewai_dashou_fencheng,
                    tupian_url=pp.get('new_tupian_url') or '',
                    paixu=int(p.paixu or 0),
                    shenhezhuangtai=1,
                    shangjia_zhuangtai=True,
                    fengjin_zhuangtai=bool(p.fengjin_zhuangtai),
                    shenhe_zhuangtai=bool(getattr(p, 'shenhe_zhuangtai', False)),
                    club_id=to_club,
                    shi_zhuanpan=bool(getattr(p, 'shi_zhuanpan', False)),
                    dianpu=None,
                    dianpu_leixing=None,
                )
                stats['products_created'] += 1

        self.stdout.write(self.style.SUCCESS(f'\n写入完成: {stats}'))
        self.stdout.write(
            '说明：未复制 Zhuanpan*；全局 ShangpinLeixing 未改；源 UFO 数据未动。'
        )

    def _list_clubs(self, Club, only_hint=False):
        qs = Club.query.order_by('club_id')
        for c in qs:
            name = c.name or ''
            if only_hint and NAME_HINT not in name:
                continue
            mark = '  <<-- 龙先生候选' if NAME_HINT in name else ''
            self.stdout.write(
                f'  club_id={c.club_id!r}  name={name!r}  status={c.status}{mark}'
            )
        if only_hint:
            hits = [c for c in Club.query.order_by('club_id') if NAME_HINT in (c.name or '')]
            if not hits:
                self.stdout.write(self.style.WARNING('  （无名称含「龙先生」的俱乐部）'))

    def _resolve_leixing(self, ShangpinLeixing, leixing_id: int, leixing_name: str):
        if leixing_id:
            row = ShangpinLeixing.query.filter(id=leixing_id).first()
            if not row:
                raise CommandError(f'找不到 ShangpinLeixing id={leixing_id}')
            if (row.jieshao or '') != leixing_name:
                raise CommandError(
                    f'安全拦截：--leixing-id={leixing_id} 的名称是「{row.jieshao}」，'
                    f'与要求的精确名「{leixing_name}」不一致。'
                )
            return row

        matches = list(ShangpinLeixing.query.filter(jieshao=leixing_name))
        if not matches:
            # 列出相近提示
            self.stdout.write('全局商品类型清单：')
            for lx in ShangpinLeixing.query.order_by('id'):
                self.stdout.write(f'  id={lx.id}  name={lx.jieshao!r}')
            raise CommandError(
                f'未找到全局类型全名精确匹配「{leixing_name}」。'
                f'请改后台类型名或 --leixing-id=... 指定。'
            )
        if len(matches) > 1:
            ids = [m.id for m in matches]
            raise CommandError(
                f'全局类型「{leixing_name}」匹配到多条 id={ids}，拒绝猜测。'
                f'请用 --leixing-id=... 指定唯一一条。'
            )
        return matches[0]
