#
# Copyright (c) Daniel Sheffield 2023
# All rights reserved
#
# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
from typing import Iterable, Dict
import os
from urllib.parse import urlencode
from bottle import (
    route,
    request,
    response,
    FormsDict,
    redirect,
    template,
    static_file,
)
from psycopg import connect
from psycopg.sql import SQL, Literal
from threading import Thread

from ..data.filter import(
    get_filter,
    get_query_param,
)

from ..data.util import(
    get_where_include_exclude
)
from . import trend as worker
from . import PARAMS
from .CachedLoadingPage import CachedLoadingPage
from .Cache import Cache

host = f"host={os.getenv('HOST')}"
db = f"dbname={os.getenv('DB', 'grocery')}"
user = f"user={os.getenv('USER', 'das')}"
password = f"password={os.getenv('PASSWORD','')}"
if not password.split('=',1)[1]:
    password = ''
conn = connect(f"{host} {db} {user} {password}")

CACHE = Cache(10)

def get_product_rollup_statement(filters, having=None):
    where = [ get_where_include_exclude(
        k[0], "name", list(include), list(exclude)
    ) for k, (include, exclude) in filters.items() ]
    return SQL('\n').join([
        SQL("""
SELECT
  count(DISTINCT p.id) AS "Products",
  count(DISTINCT c.id) AS "Categories",
  count(DISTINCT g.id) AS "Groups",
  p.name AS "Product",
  c.name AS "Category",
  g.name AS "Group"
FROM products p
JOIN categories c ON p.category_id = c.id
JOIN groups g ON c.group_id = g.id
"""),
        SQL("""
WHERE {where}
""").format(where=SQL("\nAND").join(where)) if where else SQL(''),
        SQL("""
GROUP BY ROLLUP (g.name, c.name, p.name)
"""),
        SQL("""
HAVING {having}
ORDER BY "Product", "Category", "Group"
""").format(having=having) if having else SQL('')
    ])


def normalize_query(query: FormsDict, allow: Iterable[str] = None) -> str:
    param = get_filter(query, allow=allow)
    return urlencode([
        (k, get_query_param(*param[k])) for k in sorted(param) if param[k]
    ])

def _normalize_decorator(func, poison_on_reload=False):
    def wrap(*args, **kwargs):
        _, _, path, *_ = request.urlparts
        normalized = normalize_query(request.query, allow=PARAMS)
        if poison_on_reload and request.params.get('reload') == 'true':
            CACHE.remove(normalized)
        if request.query_string != normalized:
            return redirect(f'{path}?{normalized}')
        return func(*args, **kwargs)
    return wrap

def normalize(*args, **kwargs):
    if not len(args):
        return lambda f: _normalize_decorator(f, **kwargs)
    
    return _normalize_decorator(*args)

@route('/grocery/static/<filename:path>')
def send_static(filename):
    return static_file(filename, root='app/rest/static')

@route('/grocery/trend')
@normalize(poison_on_reload=True)
def trend():
    _, _, path, *_ = request.urlparts
    normalized = normalize_query(request.query, allow=PARAMS)
    if request.params.get('reload') == 'true':
        CACHE.remove(normalized)

    if request.query_string != normalized:
        return redirect(f'{path}?{normalized}')
    
    page = CACHE.get(normalized)

    return page if page else CACHE.add(normalized, CachedLoadingPage(
        template("loading", progress=[]),
        lambda queue: Thread(target=worker.trend, args=(
            queue, conn, path, request.query
        )).start()
    ))

@route('/grocery/groups')
@normalize
def groups():
    filters = get_filter(request.query, allow=('group', 'category', 'product'))
    form = template('form-nav', action='groups', method='get', params=[
        {'name': k, 'value': request.params[k]} for k in request.params if k in PARAMS
    ])
    try:
        with conn.cursor() as cur:
            inner = get_product_rollup_statement(
                filters,
                having=SQL("c.name IS NULL")
            )
            xml = cur.execute(SQL("""
SELECT query_to_xml_and_xmlschema({q}, false, false, ''::text)
""").format(q=Literal(SQL("""SELECT
    "Products",
    "Categories",
    COALESCE("Group", "Groups"||'') "Group"
FROM (
{inner}
) q""").format(inner=inner).as_string(cur)))).fetchone()[0]
    finally:
        conn.commit()
    response.content_type = 'application/xhtml+xml; charset=utf-8'
    return template("query-to-xml", title="Groups", xml=xml, form=form)

@route('/grocery/categories')
@normalize
def categories():
    filters = get_filter(request.query, allow=('group', 'category', 'product'))
    form = template('form-nav', action='categories', method='get', params=[
        {'name': k, 'value': request.params[k]} for k in request.params if k in PARAMS
    ])
    try:
        with conn.cursor() as cur:
            inner = get_product_rollup_statement(
                filters,
                having=SQL("p.name IS NULL AND (c.name IS NOT NULL OR g.name IS NULL)")
            )
            xml = cur.execute(SQL("""
SELECT query_to_xml_and_xmlschema({q}, false, false, ''::text)
""").format(q=Literal(SQL("""SELECT
    "Products",
    COALESCE("Category", "Categories"||'') "Category",
    COALESCE("Group", "Groups"||'') "Group"
FROM (
{inner}
) q""").format(inner=inner).as_string(cur)))).fetchone()[0]
    finally:
        conn.commit()
    response.content_type = 'application/xhtml+xml; charset=utf-8'
    return template("query-to-xml", title="Categories", xml=xml, form=form)

@route('/grocery/products')
@normalize
def products():
    filters = get_filter(request.query, allow=('group', 'category', 'product'))
    form = template('form-nav', action='products', method='get', params=[
        {'name': k, 'value': request.params[k]} for k in request.params if k in PARAMS
    ])
    try:
        with conn.cursor() as cur:
            inner = get_product_rollup_statement(
                filters,
                having=SQL("p.name IS NOT NULL OR g.name IS NULL")
            )
            xml = cur.execute(SQL("""
SELECT query_to_xml_and_xmlschema({q}, false, false, ''::text)
""").format(q=Literal(SQL("""SELECT
    --"Transactions",
    COALESCE("Product", "Products"||'') "Product",
    COALESCE("Category", "Categories"||'') "Category",
    COALESCE("Group", "Groups"||'') "Group"
FROM (
{inner}
) q""").format(inner=inner).as_string(cur)))).fetchone()[0]
    finally:
        conn.commit()
    response.content_type = 'application/xhtml+xml; charset=utf-8'
    return template("query-to-xml", title="Products", xml=xml, form=form)

@route('/grocery/tags')
@normalize
def tags():
    form = template('form-nav', action='tags', method='get', params=[
        {'name': k, 'value': request.params[k]} for k in request.params if k in PARAMS
    ])
    try:
        with conn.cursor() as cur:
            inner = SQL('\n').join([SQL("""
SELECT * FROM (SELECT count(DISTINCT txn.id) AS "Uses", tg.name AS "Name"
FROM tags tg
JOIN tags_map tm ON tg.id = tm.tag_id
JOIN transactions txn ON txn.id = tm.transaction_id
GROUP BY tg.name
ORDER BY 1 DESC, 2) q
UNION ALL
SELECT count(DISTINCT txn.id) AS "Uses", count(DISTINCT tg.name)||'' AS "Name"
FROM tags tg
JOIN tags_map tm ON tg.id = tm.tag_id
JOIN transactions txn ON txn.id = tm.transaction_id
""")]).as_string(cur)
            xml = cur.execute(SQL("""
SELECT query_to_xml_and_xmlschema({inner}, false, false, ''::text)
""").format(inner=Literal(inner))).fetchone()[0]
    finally:
        conn.commit()
    response.content_type = 'application/xhtml+xml; charset=utf-8'
    return template("query-to-xml", title="Tags", xml=xml, form=form)


