"""
   @Author      : hejiangming
   @Description : 品类调研 同比(YoY)/环比(MoM) 计算。
                  读 dwt_bs_top100 的"本期 / 上月 / 去年同月"三个月分区，按分类路径对齐，
                  算 14 指标 × 2 = 28 个变化率列（12 业务指标 + 2 利润率指标）。
                  设计文档：dwt_bs_top100_概览.md 第七节、dwt_bs_top100_字段精度核对.md 第一节。
   @SourceTable : dwt_bs_top100 (本期 date_info、上月 M-1、去年同月 M-12 三个分区)
   @SinkTable   : dwt_bs_top100_change_rate
   @CreateTime  : 2026/07/14 10:00
   @UpdateTime  : 2026/07/14 10:00
"""
import os
import sys

sys.path.append(os.path.dirname(sys.path[0]))

from utils.hdfs_utils import HdfsUtils
from utils.common_util import CommonUtil
from utils.spark_util import SparkUtil
from pyspark.sql import functions as F


class DwtBsTop100ChangeRate(object):
    """
    在整个品类调研链路里的位置：
      dwt_flow_asin → dwt_bs_top100（各月落表，本表的上游）→ 本任务（跨月比）→ 导出 MySQL 给后端/前端。
    同比环比本质 = "拿本表历史期数值比"，所以必须先有历史月分区（M-1、M-12）才能算。
    """

    # ============================================================
    # 14 个参与同比环比的指标 + 计算类型（列名/顺序对齐 字段精度核对.md 29-40 行）
    #   'up'     = 越大越好，普通公式 (本期-对比期)/对比期，正数=变好
    #   'rank'   = 平均排名，唯一"越小越好"，分子调换 (对比期-本期)/对比期，使正数=排名改善
    #   'profit' = 利润率，用差值(本期率−对比期率, 涨跌几个百分点)，任一期缺失→null，不走 -1000/1000 那套
    # ============================================================
    METRICS = [
        ('asin_amazon_orders_sum',              'up'),    # 亚马逊月总销量
        ('asin_bsr_orders_sum',                 'up'),    # BSR月总销量
        ('asin_bsr_orders_mean',                'up'),    # BSR月均销量
        ('asin_bsr_orders_sum_new',             'up'),    # 新品BSR总销量
        ('asin_bsr_orders_sum_brand',           'up'),    # 品牌BSR总销量
        ('asin_count',                          'up'),    # 总产品数
        ('asin_count_new',                      'up'),    # 新品数
        ('asin_count_brand',                    'up'),    # 品牌数
        ('asin_bs_cate_1_rank_mean',            'rank'),  # 平均一级分类排名（越小越好）
        ('asin_price_mean',                     'up'),    # 平均价格
        ('asin_rating_mean',                    'up'),    # 平均评分
        ('asin_total_comments_mean',            'up'),    # 平均评论数
        ('asin_ocean_freight_gross_margin_avg', 'profit'),  # 海运平均毛利润率（需求3新增）
        ('asin_air_freight_gross_margin_avg',   'profit'),  # 空运平均毛利润率（需求3新增）
    ]

    # 跨月对齐主键：分类多归属（同 current_id 挂多个父级路径=多行），只用 current_id 会 1:N 串行。
    # current_id + parent_id_join（根→直接父级的完整路径）= 每行唯一（已实测 2026-05 分区无重复）。
    JOIN_KEYS = ['asin_bs_cate_current_id', 'asin_bs_cate_parent_id_join']

    def __init__(self, site_name, date_type, date_info):
        self.site_name = site_name
        self.date_type = date_type
        self.date_info = date_info
        self.hive_tb = "dwt_bs_top100_change_rate"
        self.source_tb = "dwt_bs_top100"
        app_name = f"{self.hive_tb}:{site_name}:{date_type}:{date_info}"
        self.spark = SparkUtil.get_spark_session(app_name)
        self.partitions_num = CommonUtil.reset_partitions(site_name, 2)

        # 环比 = 上月(M-1)，同比 = 去年同月(M-12)。月粒度用 get_month_offset 直接推，确定性强。
        self.last_month_info = CommonUtil.get_month_offset(self.date_info, -1)
        self.last_year_info = CommonUtil.get_month_offset(self.date_info, -12)
        print(f"本期={self.date_info}，环比对比期(上月)={self.last_month_info}，同比对比期(去年同月)={self.last_year_info}")

        # 落表前清 HDFS 分区目录（幂等重跑，先删后写不残留旧文件）
        hdfs_path = CommonUtil.build_hdfs_path(
            self.hive_tb,
            partition_dict={"site_name": site_name, "date_type": date_type, "date_info": date_info},
        )
        print(f"清除 hdfs 目录：{hdfs_path}")
        HdfsUtils.delete_hdfs_file(hdfs_path)

        self.df_base = self.spark.sql("select 1+1;")   # 本期
        self.df_last = self.spark.sql("select 1+1;")   # 上月（环比对比期）
        self.df_year = self.spark.sql("select 1+1;")   # 去年同月（同比对比期）
        self.df_save = self.spark.sql("select 1+1;")

    def _read_one_period(self, date_info, prefix):
        """
        读 dwt_bs_top100 一个月分区，只取 join 键 + 14 个指标列。
        prefix 非空时把指标列改名加前缀（lm_/ym_），避免和本期同名列 join 后冲突。
        """
        metric_cols = [m for m, _ in self.METRICS]
        select_cols = self.JOIN_KEYS + metric_cols
        sql = f"""
            select {', '.join(select_cols)}
            from {self.source_tb}
            where site_name='{self.site_name}' and date_type='{self.date_type}' and date_info='{date_info}'
        """
        df = self.spark.sql(sql)
        if prefix:
            for m in metric_cols:
                df = df.withColumnRenamed(m, f"{prefix}{m}")
        return df

    def read_data(self):
        # 本期额外带 level（落表用，便于后端/排查），对比期只要指标值
        metric_cols = [m for m, _ in self.METRICS]
        base_sql = f"""
            select {', '.join(self.JOIN_KEYS + ['level'] + metric_cols)}
            from {self.source_tb}
            where site_name='{self.site_name}' and date_type='{self.date_type}' and date_info='{self.date_info}'
        """
        self.df_base = self.spark.sql(base_sql).repartition(self.JOIN_KEYS[0]).cache()
        print("df_base 行数", self.df_base.count())

        self.df_last = self._read_one_period(self.last_month_info, "lm_").repartition(self.JOIN_KEYS[0]).cache()
        self.df_year = self._read_one_period(self.last_year_info, "ym_").repartition(self.JOIN_KEYS[0]).cache()

    def _rate_expr(self, cur_col, cmp_col, kind):
        """
        单个指标的变化率表达式（cur=本次/本期分子，cmp=上次/对比期分母）。真值表见文件说明/概览.md：
          空/空→null  空/0→0  空/>0→-1000(下降)  0/空、0/0→0  >0/空、>0/0→1000(上升)  其余正常算。
        利润率(profit)单独：用差值(本期率−对比期率, 即涨跌几个百分点)，任一期缺失→null(后端确认收 null)。
        """
        cur = F.col(cur_col)
        cmp = F.col(cmp_col)

        # 利润率：改用【差值】= 本期率 − 对比期率(涨跌几个百分点)，不做除法。
        #   为什么不用相对变化率：对比期为负(亏损基期)时 (本-对)/对 的符号会翻转、误导；差值无此问题，也无需防除0。
        #   任一期无数据(null) → null。base 无数据本就是 null。
        if kind == 'profit':
            # 原相对变化率(负基期符号会反,已弃用)：
            # return (F.when(cur.isNull() | cmp.isNull() | (cmp == 0), F.lit(None))
            #          .otherwise(F.round((cur - cmp) / cmp, 4)))
            return (F.when(cur.isNull() | cmp.isNull(), F.lit(None))
                     .otherwise(F.round(cur - cmp, 4)))

        # 【关键边界】业务 12 指标在 base 里"空"的编码 = null 或 -1 占位
        #   （均值列无数据存 -1，销量/计数列 0 兜底不会是 -1、也不会 null）。统一把 (null 或 -1) 当"空"。
        # base 业务列不会 null（销量/计数 0 兜底、均值列无数据=-1），"空"只需判 -1；
        # 只有对比期(cmp)会因 join 未命中变 null，故 cmp 才判 isNull。
        # cur_empty = cur.isNull() | (cur == -1)   # 原写法：cur.isNull() 对 base 永不触发，去掉避免误解
        cur_empty = (cur == -1)
        cmp_empty = cmp.isNull() | (cmp == -1)
        cur_zero = (cur == 0)
        cmp_zero = (cmp == 0)

        # 正常计算：排名越小越好→分子调换让"正数=排名改善"；其余越大越好 (本-上)/上
        if kind == 'rank':
            normal = F.round((cmp - cur) / cmp, 4)
        else:
            normal = F.round((cur - cmp) / cmp, 4)

        # 严格按真值表分支（顺序不能乱：先定本次空/0/>0，再看对比期）
        return (
            F.when(cur_empty & cmp_empty, F.lit(None))            # 空/空 → 空
             .when(cur_empty & cmp_zero, F.lit(0))                # 空/0  → 0
             .when(cur_empty, F.lit(-1000))                       # 空/>0 → -1000(本期没了=下降)
             .when(cur_zero & (cmp_empty | cmp_zero), F.lit(0))   # 0/空、0/0 → 0(无方向)
             .when(cur_zero, normal)                              # 0/>0  → 正常(=-1.0)
             .when(cmp_empty | cmp_zero, F.lit(1000))             # >0/空、>0/0 → 1000(对比期无=上升)
             .otherwise(normal)                                   # >0/>0 → 正常
        )

    def handle_data(self):
        # 本期为 base 左连对比期：本期在榜的分类才产出行；对比期缺失的行 cmp 列为 null，走上面 den_invalid 分支
        df = self.df_base.join(self.df_last, on=self.JOIN_KEYS, how='left') \
                         .join(self.df_year, on=self.JOIN_KEYS, how='left')

        # 逐指标生成 <指标>_lm_rate(环比) 和 <指标>_ym_rate(同比)
        for m, kind in self.METRICS:
            df = df.withColumn(f"{m}_lm_rate", self._rate_expr(m, f"lm_{m}", kind))
            df = df.withColumn(f"{m}_ym_rate", self._rate_expr(m, f"ym_{m}", kind))

        self.df_save = df
        self.df_base.unpersist()
        self.df_last.unpersist()
        self.df_year.unpersist()

    def save_data(self):
        # 落表 = join 键 + level + 28 个 rate 列（每指标 ym 同比、lm 环比）+ 3 分区列
        rate_cols = []
        for m, _ in self.METRICS:
            rate_cols.append(f"{m}_ym_rate")
            rate_cols.append(f"{m}_lm_rate")
        out_cols = self.JOIN_KEYS + ['level'] + rate_cols

        self.df_save = self.df_save.select(*out_cols) \
            .withColumn("site_name", F.lit(self.site_name)) \
            .withColumn("date_type", F.lit(self.date_type)) \
            .withColumn("date_info", F.lit(self.date_info))

        # 不用 auto_transfer_type：它见 Hive double 列会强制 cast 成 decimal(10,3)，把 4 位砍成 3 位，
        #   且极端大变化率(对比期是极小正数时)会超 decimal(10,3) 上限、cast 成 null（哪怕列是 double 也救不了，卡在中间那道 decimal）。
        # 表本身是 double，直接把 28 个 rate 列统一 cast 成 double 落表 —— 保留 round(4) 的 4 位精度、double 值域大不溢出。
        for c in rate_cols:
            self.df_save = self.df_save.withColumn(c, F.col(c).cast("double"))

        self.df_save = self.df_save.repartition(self.partitions_num)
        partition_by = ["site_name", "date_type", "date_info"]
        print(f"当前存储的表名为：{self.hive_tb}，分区为 {partition_by}")
        self.df_save.write.saveAsTable(name=self.hive_tb, format='hive', mode='append', partitionBy=partition_by)
        print("success")

    def run(self):
        self.read_data()
        self.handle_data()
        self.save_data()


if __name__ == '__main__':
    site_name = CommonUtil.get_sys_arg(1, None)   # 站点 us
    date_type = CommonUtil.get_sys_arg(2, None)   # month
    date_info = CommonUtil.get_sys_arg(3, None)   # 年-月，如 2026-05
    obj = DwtBsTop100ChangeRate(site_name, date_type, date_info)
    obj.run()
