import json
import logging

import persistence

from datetime import datetime, timedelta
from threading import Lock

from sysactions import get_podtype, action, get_model, StopAction
from systools import sys_log_exception

from .. import mb_context
from ..util import get_ruptured_products_from_remote_order
from pyscriptscache import cache as _cache

_product_price = {}
product_by_barcode = {}
rupture_treated_events = []
products_cache = None
last_cache_time = None
lock = Lock()
last_snapshot_id = None

logger = logging.getLogger("PosActions")


@action
def getProducts(pos_id, force=False, snapshot_id=None):
    global products_cache
    global rupture_treated_events
    global last_cache_time
    global last_snapshot_id

    force = str(force).lower() == "true"
    seconds_to_renew_cache = 3600

    if lock.locked():
        raise StopAction()

    with lock:
        now = datetime.now()
        seconds_to_renew = _get_seconds_to_renew(seconds_to_renew_cache) if not force else 0
        reached_cache_time = _reached_seconds_to_renew_cache(now, seconds_to_renew)
        changed_last_snapshot_id = snapshot_id is not None and last_snapshot_id != snapshot_id
        if not products_cache or reached_cache_time or changed_last_snapshot_id:
            last_cache_time = now
            products_cache = _get_products(pos_id)

            if snapshot_id is not None:
                last_snapshot_id = snapshot_id

    return json.dumps(products_cache)


def _get_seconds_to_renew(seconds_to_renew_cache):
    seconds_to_renew = 180
    try:
        seconds_to_renew = int(seconds_to_renew_cache)
    except (TypeError, ValueError) as _:
        pass

    return seconds_to_renew


def _rupture_is_already_handled(rupture_event_id):
    return rupture_event_id is None or rupture_event_id in rupture_treated_events


def _reached_seconds_to_renew_cache(now, seconds_to_renew_cache):
    return not last_cache_time or now > last_cache_time + timedelta(seconds=seconds_to_renew_cache)


def _get_products(pos_id):
    global product_by_barcode
    product_by_barcode = {}

    query = """SELECT P.ProductCode,
                P.ProductName,
                PCP.CustomParamValue AS BarCode,
                NULLIF(Production.JITLines,'None') AS ProductionLine,
                CASE WHEN COALESCE(PT.Tag, PCP1.CustomParamValue) IS NULL THEN 'false' ELSE 'true' END AS CFH,
                PTY.TypeDescr as ProductType
                FROM Product P
                LEFT JOIN Production ON Production.ProductCode=P.ProductCode
                LEFT JOIN ProductCustomParams PCP ON PCP.ProductCode=P.ProductCode AND PCP.CustomParamId='BarCode'
                LEFT JOIN ProductCustomParams PCP1 ON PCP1.ProductCode=P.ProductCode AND PCP1.CustomParamId='CFH'
                AND PCP1.CustomParamValue = 'true'
                LEFT JOIN ProductTags PT ON PT.ProductCode=P.ProductCode AND PT.Tag='CFH=true'
                LEFT JOIN ProductKernelParams PKP ON PKP.ProductCode=P.ProductCode
                LEFT JOIN ProductType PTY ON PTY.TypeId=PKP.ProductType
                ORDER BY P.ProductName"""
    prices = get_prices(pos_id)
    prod_list = {}

    try:
        ruptured_items = get_ruptured_products_from_remote_order()
        conn = persistence.Driver().open(mb_context, dbname=str(pos_id))
        cursor = conn.select(query)
        for row in cursor:
            plu, name, barcode, prodline, cfh, prod_type = map(row.get_entry, ("ProductCode", "ProductName", "BarCode",
                                                                               "ProductionLine", "CFH", "ProductType"))
            product = {
                "plu": plu,
                "name": name,
                "price": prices.get(plu),
                "barcode": barcode,
                "prodline": prodline,
                "ruptured": plu in ruptured_items,
                "CFH": cfh,
                "type": prod_type
            }
            prod_list[plu] = product
            if barcode:
                try:
                    product_by_barcode[int(barcode)] = product
                except Exception as _:
                    logger.warning("Invalid barcode for product '{} ({})'".format(name, plu))
    except Exception as _:
        sys_log_exception("Error getting product list")
    return prod_list


@action
def getPrices(pos_id):
    return json.dumps(get_prices(pos_id))


def get_prices(pos_id):
    part_data = {}
    tag_data = {}
    conn = persistence.Driver().open(mb_context, dbname=str(pos_id))
    part_data = _get_part_data(conn)
    tag_data = _get_tag_data(conn)

    pod_type = get_podtype(get_model(pos_id))
    from table_actions import get_price_list_from_model
    price_list = get_price_list_from_model(pos_id, pod_type=pod_type)
    prices = _get_price_data(conn, price_list)

    def get_price_by_product_type_recursively(part_entries, product, context):
        context = context if context else "1"
        price = _get_price(product, context, pod_type, price_list)
        for part_params in part_entries:
            product_type = part_params["product_type"]
            part_code = part_params["part_code"]
            default_quantity = float(part_params["default_qty"] or 1)

            full_context = "{}.{}".format(product, part_code)
            if product_type == 0:  # product
                best_price = _get_price(part_code, product, pod_type, price_list)
                price += best_price * default_quantity
            elif product_type == 1:  # option
                default_part = tag_data.get(product.split('.')[-1], {}).get(part_code)
                if default_part:
                    best_price = None
                    for default in default_part:
                        default_price = _get_price(default, full_context, pod_type, price_list)
                        best_price = min(default_price, best_price) if best_price else default_price

                    price += best_price * default_quantity
                else:
                    option_prices = []
                    for part in part_params["product_codes"]:
                        if part in part_data:
                            part_price = get_price_by_product_type_recursively(part_data[part], part, full_context)
                        else:
                            part_price = _get_price(part, full_context, pod_type, price_list)

                        option_prices.append(part_price)

                    price += min(option_prices or 0) * default_quantity
            elif product_type == 2 and part_code in part_data:
                price += get_price_by_product_type_recursively(part_data[part_code], "{}.{}".format(full_context, part_code), context)
        return price

    for product, part_entries in part_data.iteritems():
        updated_prices = {}
        if product in prices:
            for context in prices[product]:
                updated_prices.update({context: get_price_by_product_type_recursively(part_entries, product, context)})
        else:
            updated_prices.update({None: get_price_by_product_type_recursively(part_entries, product, None)})

        prices[product] = updated_prices

    return prices


def _get_price(product, context, pod_type, price_list):
    best_price = _cache.get_best_price(context, product, pod_type, price_list)
    unit_price, add_price, _ = best_price if best_price else (0, 0, 0)
    return float(unit_price or add_price or 0)


def get_product_by_barcode(barcode):
    if barcode and int(barcode) in product_by_barcode:
        return product_by_barcode[int(barcode)]

    return None


def get_product_price_from_cache(plu, pos_id):
    if _product_price == {}:
        _get_products(pos_id)
    return _product_price[int(plu)]


def _get_part_data(conn):
    part_data = {}
    cursor = conn.select("""
        SELECT
            part.productCode,
            part.partcode,
            part.defaultQty,
            params.ProductType,
            class.ProductCodes
        FROM ProductPart part
        JOIN ProductKernelParams params
            ON params.ProductCode = part.PartCode
        LEFT JOIN (
            SELECT
                classcode,
                group_concat(ProductCode) as ProductCodes
            FROM ProductClassification
            GROUP BY ClassCode
        ) class
             ON class.ClassCode = part.PartCode
        WHERE part.MinQty > 0
        ORDER BY part.ProductCode;
        """)
    for row in cursor:
        part_data.setdefault(row.get_entry(0), []).append({
            "part_code": row.get_entry(1),
            "default_qty": row.get_entry(2),
            "product_type": int(row.get_entry(3)),
            "product_codes": (row.get_entry(4) or "").split(',')
        })
    return part_data


def _get_tag_data(conn):
    tag_data = {}
    cursor = conn.select("""
        SELECT CustomAttr, PartCode, ProductCode
        FROM ProductPart
        WHERE CustomAttr LIKE '%defaultOption%'
        """)
    for row in cursor:
        product_code = row.get_entry("ProductCode")
        try:
            part_code = row.get_entry("PartCode")
            default_options_json = json.loads(row.get_entry("CustomAttr"))
            default_options = default_options_json.get("defaultOption")
            if not default_options:
                continue

            if product_code not in tag_data:
                tag_data[product_code] = {}

            tag_data[product_code].update({part_code: default_options.split("|")})
        except Exception as _:
            logger.warning("Errro getting default options for product {}".format(product_code))

    return tag_data


def _get_price_data(conn, price_list=""):
    price_data = {}

    prices_list = price_list.split('.')[::-1]
    for price_list_id in prices_list:
        cursor = conn.select("""
            SELECT ProductCode, Context, CAST(COALESCE(DefaultUnitPrice, AddedUnitPrice, 0) AS FLOAT)
            FROM Price
            WHERE PriceListId = '{}' AND DATETIME('now') BETWEEN ValidFrom AND ValidThru;
            """.format(price_list_id))
        for row in cursor:
            product_code, context, price = row
            if product_code not in price_data:
                price_data[product_code] = {context: float(price)}
            elif context not in price_data[product_code]:
                price_data[product_code][context] = float(price)
    return price_data
