import hashlib
import itertools
import logging
import random
import re
import uuid
from typing import TypedDict, Union, Sequence, Any, Literal, Optional

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.lib.posts import Post, POST_FLAG_CUT_OFF, POST_FLAG_REASON_NUMBER_TO_SLUG
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}"
    params = (
        service,
        artist_id,
        post_id,
        service,
        artist_id,
        post_id,
        service,
        artist_id,
        post_id,
    )
    query = """
        WITH main_post AS (
            SELECT
                id,
                "user",
                service,
                title,
                content,
                embed,
                shared_file,
                (
                    CASE service
                        WHEN 'fanbox'
                        THEN NULL
                        ELSE added
                    END
                ) AS added,
                published,
                edited,
                file,
                attachments,
                poll,
                captions,
                tags
            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,
        params,
        serialize_post,
        deserialize_post,
        reload,
        True,
    )


class TDPostData(TypedDict):
    service: str
    artist_id: str
    post_id: str


def get_post_multiple(input_: dict[tuple[str, str, str], TDPostData], 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: list[tuple[str, str, str]] = []
    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
                        id,
                        "user",
                        service,
                        title,
                        content,
                        embed,
                        shared_file,
                        (
                            CASE service
                                WHEN 'fanbox'
                                THEN NULL
                                ELSE added
                            END
                        ) AS added,
                        published,
                        edited,
                        file,
                        attachments,
                        poll,
                        captions,
                        tags
                    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: str, service: str, reload=True):
    key = f"post_by_id:{service}:{post_id}"
    params = dict(
        post_id=post_id,
        service=service
    )
    query = """
        SELECT
            id,
            service,
            "user"
        FROM
            posts
        WHERE
            id = %(post_id)s
            AND
            service = %(service)s
    """

    return cached_query(query, key, params, reload=reload, single=True)


def get_posts_incomplete_rewards(post_id: str, artist_id: str, service: str, reload=False):
    # todo add artist if other service needs
    key = f"posts_incomplete_rewards:{service}:{post_id}"
    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,
    )


class TDComment(TypedDict):
    id: str
    post_id: str
    parent_id: Optional[str]
    commenter: str
    service: str
    content: str
    added: str
    published: str
    deleted_at: str
    commenter_name: str


def get_post_comments(post_id, service, reload=False) -> list[TDComment]:
    if service not in ("fanbox", "patreon", "boosty"):
        return []
    key = f"comments:{service}:{post_id}"

    # we select only used fields to save memory and be faster
    if service in ("fanbox", "patreon", "boosty"):
        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
            id,
            "user",
            service,
            title,
            content,
            embed,
            shared_file,
            (
                CASE service
                    WHEN 'fanbox'
                    THEN NULL
                    ELSE added
                END
            ) AS added,
            published,
            edited,
            file,
            attachments,
            poll,
            captions,
            tags
        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"

    # extra careful building queries with strings
    assert sort in ("id", "published DESC NULLS LAST")

    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"

    # extra careful building queries with strings
    assert sort in ("id", "published DESC NULLS LAST")

    query = f"""
        SELECT
            id,
            "user",
            service,
            title,
            content,
            embed,
            shared_file,
            (
                CASE service
                    WHEN 'fanbox'
                    THEN NULL
                    ELSE added
                END
            ) AS added,
            published,
            edited,
            file,
            attachments,
            poll,
            captions,
            tags
        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 flag_post(service, creator_id, post_id, reason, flagger_id, flagger_ip) -> str | None:
    query = """
        INSERT INTO "post_flags" ("service", "creator_id", "post_id", "reason", "contributor_id", "flagger_ip_hash")
        VALUES (%s, %s, %s, %s, %s, %s)
        ON CONFLICT ("service", "creator_id", "post_id", "contributor_id")
        DO UPDATE SET "reason" = EXCLUDED."reason";
    """

    flagger_ip_hash = uuid.UUID(hashlib.sha256(flagger_ip.encode()).hexdigest()[:32])
    params = (service, creator_id, post_id, reason, flagger_id, flagger_ip_hash)

    cur = get_cursor()
    cur.execute(query, params)

    return is_post_flagged(service, creator_id, post_id, reload=True)

def is_post_flagged(service, creator_id, post_id, reload=False) -> str | None:
    key = f"is_post_flagged:{service}:{creator_id}:{post_id}"
    query = 'SELECT MAX(reason) FROM post_flags WHERE post_id = %s AND creator_id = %s AND service = %s'
    reason_int = cached_query(query, key, (post_id, creator_id, service,), unsafe_dumper, unsafe_loader, reload, lock_enabled=False, single=True)
    return POST_FLAG_REASON_NUMBER_TO_SLUG.get((reason_int or {}).get("max"))

class TDPostRevision(TypedDict):
    revision_id: int
    id: str
    user: str
    service: str
    title: str
    content: str
    embed: dict
    shared_file: bool | Literal["0"]
    added: str
    published: str
    edited: str
    file: Any
    attachments: list[Any]
    size: int
    ihash: str
    poll: Any
    tags: list[str]
    captions: Any


def get_post_revisions(service: str, artist_id: str, post_id: str, reload=False) -> list[TDPostRevision]:
    key = f"post_revisions:{service}:{artist_id}:{post_id}"
    params = dict(
        service=service,
        artist_id=artist_id,
        post_id=post_id
    )
    query = """
        SELECT
            revision_id,
            id,
            "user",
            service,
            title,
            content,
            embed,
            shared_file,
            (
                CASE service
                    WHEN 'fanbox'
                    THEN NULL
                    ELSE added
                END
            ) AS added,
            published,
            edited,
            file,
            attachments,
            poll,
            tags,
            captions
        FROM
            revisions
        WHERE
            service = %(service)s
            AND
            "user" = %(artist_id)s
            AND
            id = %(post_id)s
        ORDER BY
            revision_id DESC
    """
    return cached_query(
        query,
        key,
        params,
        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 ""


type TDPreview = Union[TDPreviewThumbnail, TDPreviewEmbed]


class TDPreviewThumbnail(TypedDict):
    type: Literal["thumbnail"]
    server: str
    name: str
    path: str


class TDPreviewEmbed(TypedDict):
    type: Literal["embed"]
    url: str
    subject: str
    description: str


class TDAttachament(TypedDict):
    server: str
    name: str
    path: str


def get_render_data_for_posts(posts: Sequence[Post]) -> tuple[list[TDPreview], list[TDAttachament], list[bool]]:
    result_previews = []
    result_attachments = []
    result_is_image = []

    for post in posts:
        previews: list[TDPreview] = []
        attachments: list[TDAttachament] = []
        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


img_replace_patterns = re.compile(pattern=r'<img\s+[^>]*?src="([^"]+)"[^>]*>')


def patch_inline_img(content):
    return img_replace_patterns.sub(replace_img_tags, content)


def replace_img_tags(match):
    src = match.group(1)
    if not src.startswith("/data"):
        src = "/data" + src
    from src.config import Configuration
    new_src = Configuration().webserver["ui"]["files_url_prepend"]["thumbnails_base_url"] + "/thumbnail" + src
    server = get_fileserver_for_value(src.split("?")[0])
    # return match.group(0).replace(src, new_src)

    return f'<a href="{server + src}" class="inlineThumb"><img src="{new_src}" data-src="{server + src}"></a>'
