import itertools
import logging
import random

from murmurhash2 import murmurhash2

from src.config import Configuration
from src.internals.cache.redis import get_conn, set_multiple_expire_keys
from src.internals.database.database import cached_count, cached_query, get_cursor
from src.internals.serializers import unsafe_dumper, unsafe_loader
from src.internals.serializers.post import (
    deserialize_post,
    deserialize_post_list,
    deserialize_posts_incomplete_rewards,
    rebuild_post_fields,
    serialize_post,
    serialize_post_list,
    serialize_posts_incomplete_rewards,
)
from src.utils.utils import fixed_size_batches, images_pattern


def get_random_post_key(table_fraction_percentage: float):
    key = f"random_post_keys:redis_list:{table_fraction_percentage}"

    redis = get_conn()
    result = redis.srandmember(key)
    if result:
        return unsafe_loader(result)

    query = "SELECT id, \"user\", service FROM posts TABLESAMPLE BERNOULLI (%s) WHERE (\"user\", service) NOT IN (SELECT id, service from dnp) and file != '{}' AND attachments != '{}'"
    random_post_keys = cached_query(
        query,
        key,
        (table_fraction_percentage,),
        lambda x: [unsafe_dumper(el) for el in x],
        lambda x: [unsafe_loader(el) for el in x],
        ex=3600,
        cache_store_method="sadd",
        lock_enabled=True,
    )
    if len(random_post_keys) == 0:
        return None
    return random.choice(random_post_keys)


def get_post(service, artist_id, post_id, reload=False):
    key = f"post:{service}:{artist_id}:{post_id}"
    query = """
        WITH main_post AS (
            SELECT *
            FROM posts
            WHERE service = %s
                AND "user" = %s
                AND id = %s
                AND ("user", service) NOT IN (SELECT id, service FROM dnp)
        )
        SELECT
            main_post.*,
            (
                SELECT id as "next"
                FROM posts
                WHERE service = %s
                    AND "user" = %s
                    AND published < (SELECT published FROM main_post)
                    AND id != %s
                ORDER BY published DESC, id DESC
                LIMIT 1
            ) AS "next",
            (
                SELECT id as "prev"
                FROM posts
                WHERE service = %s
                    AND "user" = %s
                    AND published > (SELECT published FROM main_post)
                    AND id != %s
                ORDER BY published ASC, id ASC
                LIMIT 1
            ) AS "prev"
        FROM main_post;
    """
    return cached_query(
        query,
        key,
        (
            service,
            artist_id,
            post_id,
            service,
            artist_id,
            post_id,
            service,
            artist_id,
            post_id,
        ),
        serialize_post,
        deserialize_post,
        reload,
        True,
    )


def get_post_multiple(input_, reload=False):
    if not input_:
        return []
    key = "post:{service}:{artist_id}:{post_id}"
    redis = get_conn()

    keys = [
        key.format(service=service, artist_id=artist_id, post_id=post_id) for (service, artist_id, post_id) in input_
    ]
    cache_results = redis.mget(keys)

    missing_in_cache = []
    for input_el, cache_result in zip(input_, cache_results):
        if cache_result is None:
            missing_in_cache.append(input_el)

    if not missing_in_cache:
        all_posts = [deserialize_post(el) for el in cache_results]
        return [el for el in all_posts if el]

    query = """
        WITH input_values (service, "user", id) AS (
            VALUES {input_values}
        )
        SELECT
            (
                SELECT row_to_json(x) as post
                FROM (
                WITH main_post AS (
                    SELECT *
                    FROM posts
                    WHERE service = iv.service
                        AND "user" = iv."user"
                        AND id = iv.id
                        AND ("user", service) NOT IN (SELECT id, service FROM dnp)
                )
                SELECT
                    main_post.*,
                    (
                        SELECT id as "next"
                        FROM posts
                        WHERE service = iv.service
                            AND "user" = iv."user"
                            AND published < (SELECT published FROM main_post)
                            AND id != iv.id
                        ORDER BY published DESC, id DESC
                        LIMIT 1
                    ) AS "next",
                    (
                        SELECT id as "prev"
                        FROM posts
                        WHERE service = iv.service
                            AND "user" = iv."user"
                            AND published > (SELECT published FROM main_post)
                            AND id != iv.id
                        ORDER BY published ASC, id ASC
                        LIMIT 1
                    ) AS "prev"
                FROM main_post
                ) x
                WHERE
                    x.id = iv.id
            )
        FROM
            input_values AS iv
    """

    query_result = []
    for chunk_missing_in_cache in fixed_size_batches(missing_in_cache, [10000, 1000, 200, 40, 10, 4, 1]):
        cursor = get_cursor()
        formatted_query = query.format(input_values=" , ".join("(%s, %s, %s)" for _ in chunk_missing_in_cache))
        cursor.execute(formatted_query, list(itertools.chain(*chunk_missing_in_cache)))
        query_result.extend(cursor.fetchall())

    mset_results = {}
    keys__result = {}

    if len(query_result) != len(missing_in_cache):
        raise Exception(f"len(query_result) != missing_in_cache , {len(query_result)}, {len(missing_in_cache)}")
    for r, missing in zip(query_result, missing_in_cache):
        if not r["post"]:
            mset_results[key.format(service=missing[0], artist_id=missing[1], post_id=missing[2])] = serialize_post({})
            keys__result[missing] = {}
            continue
        service = r["post"]["service"]
        user = r["post"]["user"]
        post_id = r["post"]["id"]
        k = key.format(service=service, artist_id=user, post_id=post_id)
        mset_results[k] = serialize_post(r["post"])
        keys__result[(service, user, post_id)] = r["post"]

    if not keys__result:
        logging.exception("Failed to query any fav posts", extra=dict(keys__result=keys__result))

    if mset_results:
        redis.mset(mset_results)
        set_multiple_expire_keys(mset_results)
    else:
        logging.exception("Failed to load any posts", extra=dict(missing_in_cache=missing_in_cache))

    full_result = []
    for input_el, cache_result in zip(input_, cache_results):
        if cache_result:
            if deserialized_post := deserialize_post(cache_result):
                full_result.append(deserialized_post)
        else:
            if query_result_in_result_dict := keys__result.get(input_el):
                full_result.append(rebuild_post_fields(query_result_in_result_dict))

    return full_result


def get_post_by_id(post_id, service, reload=True):
    key = f"post_by_id:{service}:{post_id}"
    query = 'select id, service, "user" from posts where id = %s and service = %s'
    return cached_query(query, key, (post_id, service), reload=reload, single=True)


def get_posts_incomplete_rewards(post_id, artist_id, service, reload=False):
    key = f"posts_incomplete_rewards:{service}:{post_id}"  # todo add artist if other service needs
    query = """
        SELECT *
        FROM posts_incomplete_rewards
        WHERE id = %s AND service = %s
    """
    return cached_query(
        query,
        key,
        (post_id, service),
        serialize_posts_incomplete_rewards,
        deserialize_posts_incomplete_rewards,
        reload,
        True,
    )


def get_post_comments(post_id, service, reload=False):
    if service not in ("fanbox", "patreon"):
        return []
    key = f"comments:{service}:{post_id}"
    # we select only used fields to save memory and be faster
    if service in ("fanbox", "patreon"):
        revisions_select = """COALESCE(json_agg(
        json_build_object(
          'id', comments_revisions.revision_id
        , 'content', comments_revisions.content
        , 'added', comments_revisions.added
        )
        ORDER BY comments_revisions.published ASC
      ) FILTER (WHERE comments_revisions.id IS NOT NULL), '[]') AS revisions"""
        revisions_join = "LEFT JOIN comments_revisions ON comments_revisions.id = comments.id"
    else:
        revisions_select = "ARRAY[]::jsonb[]"
        revisions_join = ""

    query = f"""
    SELECT
      comments.id
    , comments.parent_id
    , comments.commenter
    , comments.commenter_name
    , comments."content"
    , comments.published
    , {revisions_select}
    FROM comments
    {revisions_join}
    WHERE comments.post_id = %s AND comments.service = %s
    GROUP BY comments.id, comments.parent_id, comments.commenter, comments.commenter_name, comments."content", comments.published
    """
    return cached_query(query, key, (post_id, service), serialize_post_list, deserialize_post_list, reload)


def get_all_posts_by_artist(artist_id, service, reload=False):
    key = f"posts_by_artist:{service}:{artist_id}"
    query = """
        SELECT *
        FROM posts
        WHERE
            "user" = %s
            AND service = %s
            AND ("user", service) NOT IN (SELECT id, service from dnp);
    """
    return cached_query(
        query, key, (artist_id, service), serialize_post_list, deserialize_post_list, reload, lock_enabled=True
    )


def get_artist_posts_summary(artist_id, service, offset, limit, sort="id", reload=False):
    """we need this to render html only so we reduce data size to half redis usage"""
    key = f"artist_posts_offset:{service}:{artist_id}:{offset}:{sort}:summary"
    assert sort in ("id", "published DESC NULLS LAST")  # extra careful building queries with strings
    query = f"""
        SELECT id, "user", service, title, substring("content", 1, 50), published, file, attachments
        FROM posts
        WHERE "user" = %s AND service = %s
        ORDER BY {sort}
        OFFSET %s
        LIMIT %s
    """
    return cached_query(
        query,
        key,
        (artist_id, service, offset, limit),
        serialize_post_list,
        deserialize_post_list,
        reload,
        lock_enabled=True,
    )


def get_artist_posts_full(artist_id, service, offset, limit, sort="id", reload=False):
    key = f"artist_posts_offset:{service}:{artist_id}:{offset}:{sort}:full"
    assert sort in ("id", "published DESC NULLS LAST")  # extra careful building queries with strings
    query = f"""
        SELECT*
        FROM posts
        WHERE "user" = %s AND service = %s
        ORDER BY {sort}
        OFFSET %s
        LIMIT %s
    """
    return cached_query(
        query,
        key,
        (artist_id, service, offset, limit),
        serialize_post_list,
        deserialize_post_list,
        reload,
        lock_enabled=True,
    )


def get_artist_post_count(service, artist_id, reload=False):
    key = f"artist_post_count:{service}:{artist_id}"
    query = 'SELECT count(*) as count FROM posts WHERE "user" = %s AND service = %s'
    return cached_count(query, key, (artist_id, service), reload, lock_enabled=True)


def is_post_flagged(service, artist_id, post_id, reload=False):
    key = f"is_post_flagged:{service}:{artist_id}:{post_id}"
    query = 'SELECT COUNT(*) FROM booru_flags WHERE id = %s AND "user" = %s AND service = %s'
    return cached_count(query, key, (post_id, artist_id, service), reload)


def get_post_revisions(service, artist_id, post_id, reload=False):
    key = f"post_revisions:{service}:{artist_id}:{post_id}"
    query = 'SELECT * FROM revisions WHERE service = %s AND "user" = %s AND id = %s order by revision_id desc'
    return cached_query(
        query,
        key,
        (service, artist_id, post_id),
        serialize_post_list,
        deserialize_post_list,
        reload,
    )


def get_fileserver_for_value(value: str) -> str:
    if Configuration().webserver["ui"]["fileservers"]:
        path_hash = murmurhash2(value.encode(), 0)
        max_value = 2**32 - 1
        path_percentage = (path_hash & max_value) * 100 // max_value
        cumulative_percentage = 0
        entries: list[str | tuple[str, float]] = Configuration().webserver["ui"]["fileservers"]
        total_servers = len(entries)
        for i, entry in enumerate(entries):
            if isinstance(entry, str):
                if i >= total_servers - 1:
                    return entry
                processed_entry = (entry, (100 / total_servers) // 0.01 / 100)
            else:
                processed_entry: tuple[str, float] = entry
            name, percentage = processed_entry
            if percentage == "":
                return name
            cumulative_percentage += percentage
            if path_percentage < cumulative_percentage:
                return name
    return ""


def get_render_data_for_posts(posts):
    result_previews = []
    result_attachments = []
    result_is_image = []

    for post in posts:
        previews = []
        attachments = []
        if "path" in post["file"]:
            if images_pattern.search(post["file"]["path"]):
                result_is_image.append(True)
                previews.append(
                    {
                        "type": "thumbnail",
                        "server": get_fileserver_for_value(f"/data{post["file"]["path"]}"),
                        "name": post["file"].get("name"),
                        "path": post["file"]["path"],
                    }
                )
            else:
                result_is_image.append(False)
                attachments.append(
                    {
                        "server": get_fileserver_for_value(f"/data{post["file"]["path"]}"),
                        "name": post["file"].get("name"),
                        "path": post["file"]["path"],
                    }
                )
        else:
            result_is_image.append(False)

        if len(post.get("embed") or []):
            previews.append(
                {
                    "type": "embed",
                    "url": post["embed"]["url"],
                    "subject": post["embed"]["subject"],
                    "description": post["embed"]["description"],
                }
            )
        for attachment in post["attachments"]:
            if images_pattern.search(attachment["path"]):
                previews.append(
                    {
                        "type": "thumbnail",
                        "name": attachment.get("name"),
                        "path": attachment["path"],
                        "server": get_fileserver_for_value(f"/data{attachment["path"]}"),
                    }
                )
            else:
                attachments.append(
                    {
                        "path": attachment["path"],
                        "name": attachment.get("name"),
                        "server": get_fileserver_for_value(f"/data{attachment["path"]}"),
                    }
                )

        result_previews.append(previews)
        result_attachments.append(attachments)

    return result_previews, result_attachments, result_is_image
