"""
   @Author      : hejiangming
   @Description : 周趋势图独立计算：直读 ODS 按新口径算周趋势(排名+搜索量),写 Hive dwt_st_base_report(date_type=week)
   @SourceTable :
                  1.ods_brand_analytics          搜索词周报告(搜索词+排名+asin,按周分区)
                  2.ods_st_key                   搜索词→st_key 映射(站点级)
                  3.ods_rank_search_rate_repeat  排名→搜索量/转化率 映射(站点级)
   @SinkTable   : dwt_st_base_report (date_type=week)
   @CreateTime  : 2026/07/17 12:05
   @UpdateTime  : 2026/07/17 12:05
"""
import os
import sys

sys.path.append(os.path.dirname(sys.path[0]))  # 上级目录

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


class DwtStBaseReportWeek(object):

    def __init__(self, site_name, date_type, date_info):
        self.site_name = site_name
        self.date_type = date_type   # 固定 week
        self.date_info = date_info    # 周: 2025-24
        self.hive_tb = "dwt_st_base_report"
        app_name = f"{self.hive_tb}_week:{site_name} {date_type} {date_info}"
        self.spark = SparkUtil.get_spark_session(app_name)
        self.partitions_num = CommonUtil.reset_partitions(site_name, 1)

    def get_rank_map(self):
        """
        读 ods_rank_search_rate_repeat 产出 rank → (st_volume, st_orders) 映射。
        照搬 dim/dim_st_detail_week.py 第146-152行 + 第293-297行。
        """
        # ============================================================
        # 【为什么不用 WHERE 限日期】这张表站点级实际只有一个分区，靠「按 rank 分组 + date_info 倒序取第一行」
        #   拿每个 rank 最新那份映射，等价于直接读那个分区(与 dim 一致)
        # 【算法】周搜索量 = 月度 search_num / 4；周预估销量 = 周搜索量 × 转化率 rate(四舍五入)
        # 【例子】rank=1 月度 search_num=2768855 → st_volume=round(2768855/4)=692214
        # ============================================================
        sql = f"""
        select rank, search_num as search_volume, rate as st_search_rate, date_info
        from ods_rank_search_rate_repeat
        where site_name = '{self.site_name}'
        """
        df_map = self.spark.sql(sql)
        window = Window.partitionBy(['rank']).orderBy(df_map.date_info.desc())
        df_map = df_map.withColumn('rk', F.row_number().over(window)) \
                       .filter('rk = 1') \
                       .drop('rk', 'date_info')
        df_map = df_map.withColumn('search_volume', F.round(F.col('search_volume') / 4)) \
                       .withColumn('orders', F.round(F.col('search_volume') * F.col('st_search_rate'))) \
                       .select('rank', 'search_volume', 'orders')
        return df_map.repartition(40, 'rank').cache()

    def run(self):
        # ============================================================
        # Step1：读本周 ods_brand_analytics 的「搜索词 + 排名 + top3 asin」
        # 【为什么读 asin1/2/3】后面要照 dim 做 asin 长度过滤(剔 asin 异常词)，所以得把 asin 带出来
        # 【为什么 search_term is not null】过滤掉搜索词为空的垃圾行(与 dim/backfill 一致)
        # ============================================================
        sql = f"""
        select search_term, rank, updated_time, asin1, asin2, asin3
        from ods_brand_analytics
        where site_name = '{self.site_name}'
          and date_type = '{self.date_type}'
          and date_info = '{self.date_info}'
          and search_term is not null
        """
        df_aba = self.spark.sql(sql)

        # ============================================================
        # Step2：按搜索词去重，保留 updated_time 最新的一条(照搬 dim 第96-99行)
        # 【为什么去重】同一搜索词一周内可能有多条抓取记录，取最新那条防重复
        # 【顺序对齐 dim】dim 是「先按原始 search_term 去重、再清 \x00」，本脚本保持同序
        # ============================================================
        dedup_win = Window.partitionBy(['search_term']).orderBy(F.col('updated_time').desc_nulls_last())
        df_aba = df_aba.withColumn('dt_rank', F.row_number().over(dedup_win)) \
                       .filter('dt_rank = 1') \
                       .drop('dt_rank', 'updated_time')

        # ============================================================
        # Step3：清 \x00 脏字符(照搬 dim 第102-103行，对 search_term + asin1/2/3)
        # 【为什么】ABA 早期数据带空字节，清完仍乱码的词后面 inner join st_key 时自然被丢
        # ============================================================
        for col in ['search_term', 'asin1', 'asin2', 'asin3']:
            df_aba = df_aba.withColumn(col, F.regexp_replace(F.col(col), '\x00', ''))
        df_aba = df_aba.repartition(40, 'search_term').cache()

        # ============================================================
        # Step4：asin 长度过滤(照搬 dim save_data 第359-360行)
        # 【为什么】这是与 us 现有口径一模一样的关键一步：dim 落盘时剔掉 asin 长度>10 的异常词(约18个)，
        #   不做这步 ODS 直读会比 us 多这18个词
        # ============================================================
        df_aba = df_aba.filter('length(asin1) <= 10 AND length(asin2) <= 10 AND length(asin3) <= 10')

        # ============================================================
        # Step5：拿 st_key(照搬 dim 第156-159行)，inner join 丢掉无 key 的词
        # ============================================================
        sql = f"""
        select st_key, search_term from ods_st_key where site_name = '{self.site_name}'
        """
        df_st_key = self.spark.sql(sql).repartition(40, 'search_term').cache()

        # rank → 搜索量/销量映射
        df_map = self.get_rank_map()

        # ============================================================
        # Step6：拼装。search_term inner join st_key(与 dim save_data 一致)，再按 rank left join 映射
        # ============================================================
        df_save = df_aba.join(df_st_key, on='search_term', how='inner') \
                        .join(df_map, on='rank', how='left')

        # 补充非数据字段：年份取自 date_info，时间戳取当前
        df_save = df_save.withColumn('years', F.lit(int(self.date_info.split('-')[0]))) \
                         .withColumn('created_time', F.date_format(F.current_timestamp(), 'yyyy-MM-dd HH:mm:SS')) \
                         .withColumn('updated_time', F.date_format(F.current_timestamp(), 'yyyy-MM-dd HH:mm:SS')) \
                         .withColumn('site_name', F.lit(self.site_name)) \
                         .withColumn('date_type', F.lit(self.date_type)) \
                         .withColumn('date_info', F.lit(self.date_info))

        # ============================================================
        # Step7：空值兜底 -1(照搬 dim save_data 第430-434行)
        # 【为什么】与 us 一模一样：rank 无映射时 search_volume/orders 会是 null，dim 填 -1；
        #   实测每个 rank 都有映射、不触发，但补上保证逐字节一致
        # 【为什么用 -1】下游 Java 读 DB NULL 有问题，用 -1 占位，Java 转 null 返前端
        # ============================================================
        df_save = df_save.select(
            'st_key',
            'search_term',
            F.col('search_volume').alias('st_volume'),
            F.col('orders').alias('st_orders'),
            F.col('rank').alias('st_rank'),
            'years',
            'created_time',
            'updated_time',
            'site_name',
            'date_type',
            'date_info'
        ).fillna({
            'st_volume': -1,
            'st_orders': -1,
            'st_rank': -1
        })

        # ============================================================
        # Step8：落盘。先物理删该分区目录再 append，保证幂等
        # 【为什么先删】saveAsTable append 不覆盖同分区旧文件，重跑会叠加重复数据
        # ============================================================
        hdfs_path = f"/home/{SparkUtil.DEF_USE_DB}/dwt/{self.hive_tb}/site_name={self.site_name}/date_type={self.date_type}/date_info={self.date_info}"
        print(f"清除hdfs目录中数据:{hdfs_path}")
        HdfsUtils.delete_hdfs_file(hdfs_path)

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


if __name__ == '__main__':
    site_name = CommonUtil.get_sys_arg(1, None)   # 参数1：站点 us/uk/de
    date_type = CommonUtil.get_sys_arg(2, None)   # 参数2：固定 week
    date_info = CommonUtil.get_sys_arg(3, None)   # 参数3：周 2025-24
    obj = DwtStBaseReportWeek(site_name, date_type, date_info)
    obj.run()
