# -*- coding: utf8 -*-

import time
import struct
from hashlib import md5
import yt.yson as yson


def get_partition(val):
    "(String) -> Int"
    val = md5(val).hexdigest()[-1]
    return int(val, base=16)


def datetime2python_datetime(s):
    "(String) -> datetime"
    return time.strptime(s, "%Y-%m-%d %H:%M:%S")


def datetime2unixtimestamp(s):
    "(String?) -> YsonUint64?"
    if not s:
        return None
    
    dt = datetime2python_datetime(s)
    return yson.YsonUint64(time.mktime(dt))


def datetime2hour(s):
    "(String?) -> Int?"
    if not s:
        return None

    dt = datetime2python_datetime(s)
    return dt.tm_hour


def datetime2date(s):
    "(String?) -> String?"
    if not s:
        return None
    
    dt = datetime2python_datetime(s)
    return time.strftime("%Y-%m-%d", dt)


def idx_max_of(xs):
    u"""
    ([String?]) -> Int32?
    Возвращаем индекс максимального элемента в массиве xs
    """
    xs = [float(x.strip()) for x in xs if x]
    if xs:
        return xs.index(max(xs))
    else:
        return None


def get_maybe_int(val):
    "(String?) -> int?"
    if not val:
        return None
    else:
        return int(val)
    

def get_maybe_uint(val):
    "(String?) -> YsonUint64?"
    if not val:
        return None
    else:
        return yson.YsonUint64(val)


def get_maybe_float(val):
    "(String?) -> float?"
    if not val:
        return None
    else:
        return float(val)


def parse_dsv(s, val_sep, kv_sep):
    """
    (String?, String, String) -> Dict

    >>> parse_dsv("desktop=false&isMobile=true", "&", "=")
    {'desktop': 'false', 'isMobile': 'true'}
    """
    if not s:
        return dict()

    s = s.split(val_sep)
    s = [x.split(kv_sep, 1) for x in s if x]
    
    return dict([x for x in s if len(x) == 2])


def decode_regular_coord(encoded_points):
    """
    (String?) -> List<Double>?
    # (String?) -> List<Tuple<Double, Double>>?
    Расшифровывает регулярные координаты пользователя, сохраненные как бинарные данные в кодировке base64.


    Note:
    Описание формата https://wiki.yandex-team.ru/Awaps/SocialDemo/#formatxranenijaprofiljavbazedannyx
    В июне 2016 считалось, что координата считается регулярной, если пользователь накопил 4 часа за 44 дня в этой точке (радиус?). 

    user_regular_location - регулярные геокоординаты, формат:
        {{4 байта: Lat * 1E7 - широта точки (целое число)}{4 байта: Lon * 1E7 - долгота точки (целое число)}}

    Числа в big-endian (начиная со старшего байта)

    Example:
    >>> decode_regular_coord("H7GLshFmotYfs9i4EWpaDA==")
    [(53.172933, 29.193903), (53.188012, 29.218254)]

    >>> decode_regular_coord("IY4YiBooMVo=")
    [(56.296052, 43.884169)]

    >>> decode_regular_coord(None), decode_regular_coord("")
    ([], [])
    """
    if not encoded_points:
        return None

    decoded = encoded_points.decode("base64")
    lat_lon = map(lambda x: x/1e7, struct.unpack(">%si" % (len(decoded) / 4), decoded))
    return lat_lon


def decode_search_cats(encoded_cats):
    """decode_search_cats
    (String?)->List<Int64>?
    Расшифровывает мкб-категории, сохраненные как бинарные данные в кодировке base64.


    Note:
    Описание формата https://wiki.yandex-team.ru/Awaps/SocialDemo/#formatxranenijaprofiljavbazedannyx
    Справочник категорий https://aw-admin.yandex-team.ru/administrator/mcb_category/index.jsp

    user_search_cats - поисковые (МКБ?) категории, формат:
        {4 байта: номер категории (целое число)}...;

    Числа в big-endian (начиная со старшего байта).

    Example:
    >>> decode_search_cats("BfXhbwX14XIF9eF2BfXhjgX14Y8F9eGVBfXhqQX14jcF9eI8BfXiQQX14mwF9eKk"))
    [100000111, 100000114, 100000118, 100000142, 100000143, 100000149, 100000169, 100000311, 100000316, 100000321, 100000364, 100000420]
    """

    if not encoded_cats:
        return None

    decoded = encoded_cats.decode("base64")
    return struct.unpack(">%si" % (len(decoded) / 4), decoded)


def decode_adhoc_prob(encoded_adhoc_prob):
    """
    (String?)->List<Uint32>?
    Расшифровывает вероятностные сегменты, сохраненные как бинарные данные в кодировке base64.


    Note:
    Описание формата https://wiki.yandex-team.ru/Awaps/SocialDemo/#formatxranenijaprofiljavbazedannyx
    Справочник коммерческих сегментов https://wiki.yandex-team.ru/crypta/crypta-da/segments/segments/
      keyword 217 - вероятностные model_id; про остальные keyword https://wiki.yandex-team.ru/Crypta/OutputToBB/#formatstrokivnutripachki

    Равнозначные названия - model_id, <id сегментной категории>, <id категории>.

    user_adhoc_prob - вероятностные сегменты крипты, формат:
        {первый байт: количество model_id},
        {{2 байта: model_id}{1 байт: число сегментов в model_id}}^n, где n - количество model_id
        {{1 байт: вероятность (0-100) для i-го сегмента в model_id}^m}^n, где n - количество model_id, m - количество сегментов в model_id;

    Числа в big-endian (начиная со старшего байта).

    Вероятности сегментов и сами <сегментные индексы> - внутренняя кухня Крипты. Для подавляющего большинства 
    сегментов/model_id будет одно значение вероятности. В логи крипты и логи авапса попадают только значения
    вероятностей, которые оказались выше порога. Поэтому если для этого yuid'а указан какой-то сегмент и 
    соответствующая вероятность, то мы отнесли этого пользователя к этому сегменту.

    В справочнике выше могут быть не все сегменты. Например, 193, 316 и 343 это наши внутренние маркетинговые 
    сегменты и статистика по ним для медиапланирования не нужна.
    """
    if not encoded_adhoc_prob:
        return None

    decoded = encoded_adhoc_prob.decode("base64")

    # {1 байт: число категорий}
    cat_count = struct.unpack(">B", decoded[0])[0]

    # [{2 байта: номер категории/model_id/id сегментной категории}{1 байт: число сегментов в категории}]
    cats_id_cnt = struct.unpack(">" + "HB" * cat_count, decoded[1:cat_count * 3 + 1]) # 2 байта + 1 байт = 3 байта

    return cats_id_cnt[::2]


def decode_adhoc_strict(encoded_cat_strict):
    """
    (String?) -> List<Uint32>?
    Расшифровывает строгие сегменты, сохраненные как бинарные данные в кодировке base64.


    Note:
    Описание формата https://wiki.yandex-team.ru/Awaps/SocialDemo/#formatxranenijaprofiljavbazedannyx
    Справочник коммерческих сегментов https://wiki.yandex-team.ru/crypta/crypta-da/segments/segments/
      keyword 216 - строгие model_id; про остальные keyword https://wiki.yandex-team.ru/Crypta/OutputToBB/#formatstrokivnutripachki

    Равнозначные названия - model_id, <id сегментной категории>, <id категории>.

    user_adhoc_strict - строгие сегменты крипты, формат:
        {{2 байта: model_id}{1 байт: номер сегмента}};

    Числа в big-endian (начиная со старшего байта).

    В adhocs_strict попадают эвристические ("строгие") сегменты. У них нет значений веротностей, а есть только их
    наличие или отсутствие. В логах крипты (0 - нет, 1 - есть, в логи попадают только сегменты с "единичками"), в логах 
    авапса (1 - нет, 2 - есть, в логи соответственно должны попадать только сегменты с "двоечками"). На данный момент
    (2016-09-13) логика для эвристических сегментов такая.
    """
    if not encoded_cat_strict:
        return None

    decoded = encoded_cat_strict.decode("base64")
    cats_id_segm_id = struct.unpack(">" + (len(decoded) / 3) * "HB", decoded)

    return cats_id_segm_id[::2]


def decode_goals(encoded_goals):
    """
    (String?) -> List<Uint64>?
    Расшифровывает номера достигнутых целей, сохраненные как бинарные данные в кодировке base64.


    Note:
    Описание формата https://wiki.yandex-team.ru/Awaps/SocialDemo/#formatxranenijaprofiljavbazedannyx
    Справочник для поведенческого ретаргетинга https://wiki.yandex-team.ru/Sales/supportofcommercialservices/support/display/tematarget/

    user_goals - цели Метрики для ретаргетинга:, формат:
        { {4 байта: номер цели}{2 байта: время в часах с достижения цели до записи в лог} };

    Числа в big-endian (начиная со старшего байта).

    Example:
    >>> decode_goals("ABs8fwVzAHo9jwg8AV+oYAAO")
    {1784959: 1395, 8011151: 2108, 23046240: 14}

    >>> decode_goals(""), decode_goals(None)
    ({}, {})
    """
    if not encoded_goals:
        return None

    decoded = encoded_goals.decode("base64")
    goals_id_hours =  struct.unpack(">" + (len(decoded) / 6) * "IH", decoded)  # 6 = 4 байта + 2 байта

    return goals_id_hours[::2]


def mapper(rec):
    
    #todo(n-bar) Need to check missed columns
    has_action = rec.get("actionid") in {"0", "11", "130"}
    has_product = get_maybe_int(rec.get("sectionid")) not in {216, 217, None}
    has_reg_location = rec.get("user_regular_location", "") != ""
    has_act_location = rec.get("lat", "") != "" and rec.get("lon", "") != ""
    
    is_valid_row = has_action and has_product and (has_reg_location or has_act_location)

    if is_valid_row:
        
        new_rec = dict()
        new_rec["sample_6p"] = get_partition(rec.get("userid"))
        new_rec["userid"] = get_maybe_uint(rec.get("userid"))
        new_rec["actionid"] = int(rec.get("actionid"))
        # Номер секции
        new_rec["sectionid"] = get_maybe_int(rec.get("sectionid"))
        # Актуальные координаты
        new_rec["lat"] = get_maybe_float(rec.get("lat"))
        new_rec["lon"] = get_maybe_float(rec.get("lon"))
        # Только для 0 и 95 actionid. Стоимость показа, можно использовать для расчета CPM. Нет когда rtb_host_id = 0. 
        new_rec["rtb_stlm_price"] = get_maybe_int(rec.get("rtb_stlm_price"))
        
        parsed_parameterstr = parse_dsv(rec.get("parameterstr"), "&", "=")
        isMobile = parsed_parameterstr.get("isMobile")
        new_rec["is_mobile"] = None if isMobile is None else isMobile in {1, "1", "True", "true", "t", "T", True}
        
        # Регулярные координаты
        new_rec["user_regular_location"] = decode_regular_coord(rec.get("user_regular_location")) # List<Double>
        # Поисковый ретаргетинг
        new_rec["user_search_cats"] = decode_search_cats(rec.get("user_search_cats")) # List<Int64>
        # Вероятностные сегменты
        new_rec["user_adhoc_prob"] = decode_adhoc_prob(rec.get("user_adhoc_prob"))  # List<Uint32>
        # Строгие сегменты
        new_rec["user_adhoc_strict"] = decode_adhoc_strict(rec.get("user_adhoc_strict"))  # List<Uint32>
        # Поведенческий ретаргетинг
        new_rec["user_goals"] = decode_goals(rec.get("user_goals"))  # List<Uint64>
        # Соц. дем.
        new_rec["sd_gender"] = bool(idx_max_of([rec.get("sd_gender_f"), rec.get("sd_gender_m")]))
        new_rec["sd_age"] = idx_max_of([rec.get("sd_age_0"), rec.get("sd_age_18"), rec.get("sd_age_25"), rec.get("sd_age_35"), rec.get("sd_age_45")])
        new_rec["sd_income"] = idx_max_of([rec.get("sd_income_a"), rec.get("sd_income_b"), rec.get("sd_income_c")])
    
        yield new_rec


if __name__ == "__main__":
    
    from email.mime.text import MIMEText
    from subprocess import Popen, PIPE
    import os
    import yt.wrapper as yt
    
    
    KEEPED_DAYS = 20
    OPERATION_WEIGHT = 0.5
    OPERATION_TITLE = "Geostat (daily update)"
    SOURCE_DIR = "//statbox/awaps-log/"
    TARGET_DIR = "//home/vipplanners/n-bar/geostat/"
    YT_PROXY = "hahn.yt.yandex.net"
    with open("/home/n-bar/.yt/token", "r") as src:
        YT_TOKEN = src.readline().strip()

    yt.config.set_proxy(YT_PROXY)
    yt.config.http.TOKEN = YT_TOKEN
    yt.config.PYTHON_DO_NOT_USE_PYC = True
    print 'host', yt.config.http.PROXY
    schema = [
              {'name': 'sample_6p',                   'type': 'int64',   "group": "mp"},
              {'name': 'userid',                      'type': 'uint64',  "group": "mp"},
              {'name': 'actionid',                    'type': 'int64',   "group": "mp"},
              {'name': 'sectionid',                   'type': 'int64',   "group": "mp"},
              {'name': 'lat',                         'type': 'double',  "group": "act_geo"},
              {'name': 'lon',                         'type': 'double',  "group": "act_geo"},
              {'name': 'rtb_stlm_price',              'type': 'int64',   "group": "mp"},
              {'name': 'is_mobile',                   'type': 'boolean', "group": "mp"},
              {'name': 'user_regular_location',       'type': 'any'},
              {'name': 'user_search_cats',            'type': 'any'},
              {'name': 'user_adhoc_prob',             'type': 'any'},
              {'name': 'user_adhoc_strict',           'type': 'any'},
              {'name': 'user_goals',                  'type': 'any'},
              {'name': 'sd_gender',                   'type': 'boolean', "group": "sd"},
              {'name': 'sd_age',                      'type': 'int64',   "group": "sd"},
              {'name': 'sd_income',                   'type': 'int64',   "group": "sd"}
             ]
    schema = yt.yson.YsonList(schema)
    schema.attributes["strict"] = True
    
    
    def send_notification(from_name, send_to, subject, msg):

        msg = MIMEText(msg)
        msg["From"] = from_name
        msg["To"] = send_to
        msg["Subject"] = subject
        p = Popen(["/usr/sbin/sendmail", "-t", "-oi"], stdin=PIPE)
        p.communicate(msg.as_string())

        return True
    

    def clean_account_folder():

        KEEP_DAYS = KEEPED_DAYS
        
        removed_list = sorted(yt.list(TARGET_DIR[:-1]))[:-KEEP_DAYS]

        if removed_list:
            map(lambda date: yt.remove(TARGET_DIR + date), removed_list)
            print "remove: %s" % removed_list
            send_notification("geostat", "n-bar@yandex-team.ru", "remove: success", "remove: %s" % removed_list)

        return True


    def get_next_table():
        KEEP_DAYS, DELAY_DAYS = KEEPED_DAYS, 2  # 7, 2 by default
        source_list = sorted(yt.list(SOURCE_DIR[:-1]))[-DELAY_DAYS - KEEP_DAYS:-DELAY_DAYS]
        target_list = sorted(yt.list(TARGET_DIR[:-1]))
        source_list = [x for x in source_list if x not in target_list]
        
        if source_list:
            print "planned: %s " % source_list

        return source_list
    
    
    col = ["userid", "actionid", "sectionid", "lat", "lon", "rtb_stlm_price", "parameterstr",
           "user_regular_location", "user_search_cats", "user_adhoc_prob", "user_adhoc_strict", "user_goals",
           "sd_gender_f", "sd_gender_m", "sd_age_0", "sd_age_18", "sd_age_35", "sd_age_45",
           "sd_income_a", "sd_income_b", "sd_income_c"]
 
    while True:
        
        try:
            clean_account_folder()
        except Exception as err:
            send_notification("geostat", "n-bar@yandex-team.ru", "Clean: error", "something went wrong:\n%s" % err)
            
        date = get_next_table()

        if not date:
            break

        date = date[0]
        source_table = yt.TablePath(SOURCE_DIR + date, columns=col)
        target_table = yt.TablePath(TARGET_DIR + date)
        

        with yt.Transaction():
            
            yt.create_table(target_table,
                            recursive=True,
                            attributes={"optimize_for": "scan",
                                        "erasure_codec": "lrc_12_2_2",
                                        "compression_codec": "lz4",  # lz4 by default
                                        "schema": schema})
            print "%s - created" % target_table

            # Run transformation
            yt.run_map(mapper,
                       source_table,
                       target_table,
                       spec={"weight": OPERATION_WEIGHT,
                             "title": OPERATION_TITLE,
                             "max_failed_job_count": 25,
                             "data_size_per_job": 2 * 1024 * 1024 * 1024,  # N GB
                             "data_size_per_map_job": 2 * 1024 * 1024 * 1024,  # N GB
                             "map_selectivity_factor": 0.3
                            })
            print "%s - mapped" % date
            yt.run_sort(target_table, #"<schema={0}>{1}".format(yt.yson.dumps(schema), target_table),
                        sort_by=["sample_6p", "userid"],
                        spec={"weight": OPERATION_WEIGHT, "title": OPERATION_TITLE}
                       )
            print "%s - sorted" % date
            yt.run_merge(target_table, 
                         target_table, 
                         spec={"weight": OPERATION_WEIGHT, "title": OPERATION_TITLE, "combine_chunks": True}
                        )
            print "%s - merged" % date
            try:
                row_count = yt.get_attribute(target_table, "row_count")
            except:
                row_count = None

            send_notification("geostat", "n-bar@yandex-team.ru", "MR: success. %s" % TARGET_DIR, "%s is finished, rows: %s" % (target_table, row_count))
            
    try:
        clean_account_folder()
    except Exception as err:
        send_notification("geostat", "n-bar@yandex-team.ru", "Clean: error", "something went wrong:\n%s" % err)
