import datetime
import json
import re
import logging
from typing import cast, get_args, TypedDict
from pathlib import PurePath

import dateutil.parser
from datetime import date
from flask import jsonify, make_response, request, url_for, redirect, session

from src.config import Configuration
from src.lib.artist import get_artist
from src.lib.post import (
    flag_post,
    get_artist_posts_full,
    get_post,
    get_post_revisions,
    get_random_post_key,
    get_post_by_id,
    get_render_data_for_posts,
    get_fileserver_for_value,
    get_posts_incomplete_rewards,
    is_post_flagged,
    patch_inline_img,
    TDPostRevision
)
from src.lib.posts import get_all_posts_for_query, get_all_posts_summary, get_popular_posts_for_date_range, \
    get_all_tags, count_all_posts, Post, count_all_posts_for_query, get_tagged_posts, count_all_posts_for_tag, \
    POST_FLAG_REASON_SLUG_TO_NUMBER
from src.lib.files import get_archive_files
from src.lib.api import create_not_found_error_response, create_client_error_response
from src.pages.artists import do_artist_post_search
from src.pages.post import ready_post_props
from src.utils.datetime_ import PeriodScale, parse_scale_string
from src.utils.utils import get_query_parameters_dict, parse_int, positive_or_none, step_int, set_query_parameter, images_pattern, sanitize_html, limit_int

from src.pages.api.v1 import v1api_bp


@v1api_bp.get("/<service>/post/<post_id>")
def get_by_id(service, post_id):
    post = get_post_by_id(post_id, service)

    if not post:
        message = "No post found"
        response = make_response(jsonify(error=message), 404)

        return response

    response = make_response(
        jsonify(
            service=post["service"],
            artist_id=post["user"],
            post_id=post["id"],
        ),
        200,
    )
    response.headers["Cache-Control"] = "s-maxage=86400"

    return response


@v1api_bp.get("/<service>/user/<creator_id>/posts")
@v1api_bp.get("/<service>/user/<creator_id>")
def list_posts_api(service, creator_id):
    limit = 50
    offset = positive_or_none(step_int(parse_int(request.args.get("o"), 0), limit))
    if offset is None:
        response = make_response(jsonify({"error": "offset not multiple of 50"}), 400)
        return response

    query = request.args.get("q", default="").strip()

    if not query or len(query) < 2:
        posts = get_artist_posts_full(creator_id, service, offset, limit, "published DESC NULLS LAST")
    else:
        (posts, total_count) = do_artist_post_search(creator_id, service, query, offset, limit)
    response = make_response(jsonify(posts), 200)

    return response


video_extensions = Configuration().webserver["ui"]["video_extensions"]


@v1api_bp.get("/<service>/user/<creator_id>/post/<post_id>")
def get_post_api(service, creator_id, post_id):
    post = get_post(service, creator_id, post_id)

    if not post or post["user"] != creator_id:
        return create_not_found_error_response()

    attachments, previews, videos, props = ready_post_props_light(post)
    response = make_response(
        jsonify(
            post=post,
            attachments=attachments,
            previews=previews,
            videos=videos,
            props=props
        ),
        200
    )
    response.headers["Cache-Control"] = "s-maxage=60"

    return response


@v1api_bp.get("/<service>/user/<creator_id>/post/<post_id>/revisions")
def list_post_revision_api(service, creator_id, post_id):
    revisions = get_post_revisions(service, creator_id, post_id)
    response = make_response(jsonify(revisions), 200 if revisions else 404)
    response.headers["Cache-Control"] = "max-age=600"

    return response


@v1api_bp.route("/<service>/user/<artist_id>/post/<post_id>/revision/<revision_id>")
def get_post_revision(service: str, artist_id: str, post_id: str, revision_id: str):
    revisions = get_post_revisions(service, artist_id, post_id) if revision_id.isdigit() else []
    revision = next((rev for rev in revisions if rev["revision_id"] == int(revision_id)), None)

    if not revision or not (
        service == revision["service"] and artist_id == revision["user"] and post_id == revision["id"]
    ):
        message = "No post revision found"
        response = make_response(jsonify(error=message), 404)

        return response

    attachments, comments, previews, videos, props = ready_post_props(revision)
    props["currentPage"] = "revisions"

    response = make_response(
        jsonify(
            props=props,
            post=revision,
            comments=comments,
            result_previews=previews,
            result_attachments=attachments,
            videos=videos,
            archives_enabled=Configuration().archive_server["enabled"],
        ),
        200,
    )
    response.headers["Cache-Control"] = "s-maxage=600"

    return response


@v1api_bp.post("/<service>/user/<user_id>/post/<post_id>/flag")
def flag_post_api(service, user_id, post_id):
    flagger_id = session.get("account_id")
    if not flagger_id:
        return make_response(jsonify(error="Not logged in"), 401)

    body = request.get_json()
    reason = body.get("reason")

    if not reason:
        return make_response(jsonify(error="No reason provided"), 400)

    if reason not in POST_FLAG_REASON_SLUG_TO_NUMBER:
        return make_response(jsonify(error="Invalid reason provided"), 400)

    result = flag_post(service, user_id, post_id, POST_FLAG_REASON_SLUG_TO_NUMBER[reason], flagger_id, request.remote_addr)
    return make_response(jsonify(result), 200)


@v1api_bp.get("/posts")
def recent():
    limit = 50
    query_params = get_query_parameters_dict(request, on_errors="ignore", clean_query_string=True)

    extra_pages = Configuration().webserver["extra_pages_to_load_on_posts"]
    max_offset = limit * 1000  # only load 1000 pages of any result
    query = query_params.get("q", "").strip()[: Configuration().webserver["max_full_text_search_input_len"]]
    tags = request.args.getlist("tag")
    o = query_params.pop("o", 0)
    offset = positive_or_none(step_int(parse_int(o, 0), limit))

    if offset is None or offset > max_offset:
        return create_client_error_response("offset not multiple of 150 or too large")

    extra_offset = positive_or_none(step_int(parse_int(o, 0), limit * extra_pages))
    slice_offset = offset - extra_offset
    count = 0
    true_count = 0

    if tags:
        extra_results = get_tagged_posts(tags, extra_offset, limit * extra_pages)
        total_count = count_all_posts_for_tag(tags)
        true_count = total_count
        count = limit_int(total_count, max_offset)

    elif not query or len(query) < 2:
        extra_results = get_all_posts_summary(
            extra_offset, limit * extra_pages, cache_ttl=Configuration().cache_ttl_for_recent_posts
        )[slice_offset : limit + slice_offset]
        true_count = count_all_posts()
        count = limit_int(true_count, max_offset)

    else:
        try:
            extra_results = get_all_posts_for_query(query, extra_offset, limit * extra_pages)
        except Exception as error:
            if "failed to parse expression" not in str(error):
                raise
            query = "Failed to parse query."
            query_params["q"] = query
            extra_results = get_all_posts_for_query(query, extra_offset, limit * extra_pages)

        if not offset and len(extra_results) < limit:
            true_count = 0
            count = len(extra_results)
        else:
            try:
                true_count = count_all_posts_for_query(query)
                count = limit_int(true_count, max_offset)
            except Exception as count_error:  # catch timeouts, set count as max offset
                logging.exception(
                    "Caught error in count_all_posts_for_query",
                    extra={"e": count_error},
                )
                true_count = 0
                count = max_offset

    results = extra_results[slice_offset : limit + slice_offset]
    response = make_response(
        jsonify(
            count=count,
            true_count=true_count,
            posts=results
        ),
        200
    )
    response.headers["Cache-Control"] = "max-age=60, public, stale-while-revalidate=2592000"

    return response


@v1api_bp.get("/posts/random")
def random_post():
    post = get_random_post_key(Configuration().webserver.get("table_sample_bernoulli_sample_size"))

    if post is None:
        message = "No post found"
        response = make_response(jsonify(error=message), 404)

        return response

    response = make_response(
        jsonify(
            service=post["service"],
            artist_id=post["user"],
            post_id=post["id"],
        ),
        200,
    )

    return response


@v1api_bp.get("/posts/popular")
def popular_posts():
    query_params = get_query_parameters_dict(request)
    earliest_date_for_popular: date = Configuration().webserver.get("earliest_date_for_popular")
    # checked below but doesn't typecheck without a cast
    scale = cast(PeriodScale, query_params.get("period", "recent"))

    if scale not in get_args(PeriodScale):
        scale = "recent"

    info, valid_date = parse_scale_string(query_params.get("date"), scale)
    does_not_match_step_date = scale != "recent" and info.date.date() != info.navigation_dates[scale][2]

    if (
        not valid_date
        or does_not_match_step_date
        or info.date.date() > datetime.date.today()
        or info.date.date() < earliest_date_for_popular
    ):
        correct_date = info.navigation_dates[scale][2].isoformat()

        if info.date.date() > datetime.date.today():
            correct_date = datetime.date.today()
            scale = "day"

        elif info.date.date() < earliest_date_for_popular:
            correct_date = earliest_date_for_popular
            scale = "day"

        new_url = set_query_parameter(url_for("api.v1.popular_posts"), {"date": correct_date, "period": scale})
        response = redirect(new_url)
        cache_seconds = int(datetime.timedelta(days=7).total_seconds())

        if info.date.date() > datetime.date.today():
            cache_seconds = int(datetime.timedelta(hours=3).total_seconds())

        response.headers["Cache-Control"] = f"max-age={cache_seconds}"

        return response

    expiry = int(datetime.timedelta(days=30).total_seconds())

    if scale == "recent":
        expiry = int(datetime.timedelta(minutes=30 + 1).total_seconds())
    elif info.max_date > datetime.datetime.utcnow():
        if scale == "day":
            expiry = int(datetime.timedelta(hours=3).total_seconds())

        elif scale == "week":
            expiry = int(datetime.timedelta(days=1).total_seconds())

        elif scale == "month":
            if datetime.date.today().day < 7:
                expiry = int(datetime.timedelta(days=1).total_seconds())
            else:
                expiry = int(datetime.timedelta(days=5).total_seconds())

    pages = Configuration().webserver.get("pages_in_popular")
    per_page = 50
    offset = positive_or_none(step_int(parse_int(query_params.pop("o", 0), 0), per_page))

    if offset is None:
        response = redirect(url_for("posts.popular_posts"))
        response.headers["Cache-Control"] = f"max-age={int(datetime.timedelta(days=7).total_seconds())}"

        return response

    posts = get_popular_posts_for_date_range(
        info.min_date, info.max_date, scale, offset // per_page, per_page, pages, expiry
    )
    (previews, attachments, is_image) = get_render_data_for_posts(posts)
    props = dict(
        currentPage="popular_posts",
        today=datetime.date.today(),
        earliest_date_for_popular=Configuration().webserver.get("earliest_date_for_popular"),
        limit=per_page,
        count=pages * per_page,
    )

    response = make_response(
        jsonify(
            info=info,
            props=props,
            results=posts,
            base=query_params,
            result_previews=previews,
            result_attachments=attachments,
            result_is_image=is_image,
        ),
        200,
    )
    response.headers["Cache-Control"] = f"max-age={int(expiry)}"

    return response


@v1api_bp.get("/posts/tags")
def list_tags():
    props = dict(currentPage="tags")
    response = make_response(
        jsonify(
            props=props,
            tags=get_all_tags(),
        ),
    )
    response.headers["Cache-Control"] = "s-maxage=3600"

    return response


DOWNLOAD_URL_FANBOX_REGEX = re.compile(r"<img\s+src=[\"']https:\/\/downloads\.fanbox\.cc\/.*?['\"]\/>")


class TDPostProps(TypedDict):
    flagged: str
    revisions: list[TDPostRevision]


def ready_post_props_light(post: Post):
    service = post["service"]
    artist_id = post["user"]
    post_id = post["id"]

    if service in ("patreon",):

        if post["file"] and post["attachments"] and post["file"] == post["attachments"][0]:
            post["attachments"] = post["attachments"][1:]

    if service in ("fansly", "onlyfans"):
        posts_incomplete_rewards = get_posts_incomplete_rewards(post_id, artist_id, service)

        if posts_incomplete_rewards:
            post["incomplete_rewards"] = "This post is missing paid rewards from a higher tier or payment."

            if post["service"] == "onlyfans":
                try:
                    rewards_info_text = (
                        f"{posts_incomplete_rewards["incomplete_attachments_info"]["media_count"]} media, "
                        f"{posts_incomplete_rewards["incomplete_attachments_info"]["photo_count"]} photos, "
                        f"{posts_incomplete_rewards["incomplete_attachments_info"]["video_count"]} videos, "
                        f"for {posts_incomplete_rewards["incomplete_attachments_info"]["price"]}$."
                    )
                    post["incomplete_rewards"] += "\n" + rewards_info_text
                except Exception:
                    pass

            elif post["service"] == "fansly":
                try:
                    rewards_info_text = (
                        f"Downloaded:{posts_incomplete_rewards["incomplete_attachments_info"]["complete"]} "
                        f"Missing:{posts_incomplete_rewards["incomplete_attachments_info"]["incomplete"]}"
                    )
                    post["incomplete_rewards"] += "\n" + rewards_info_text
                except Exception:
                    pass
    previews = []
    attachments = []
    videos = []

    if "path" in post["file"]:

        if images_pattern.search(post["file"]["path"]):
            previews.append(
                {
                    "type": "thumbnail",
                    "server": get_fileserver_for_value(f"/data{post["file"]["path"]}"),
                    "name": post["file"].get("name"),
                    "path": post["file"]["path"],
                }
            )
        else:
            file_extension = PurePath(post["file"]["path"]).suffix
            name_extension = PurePath(post["file"].get("name") or "").suffix
            # filename without extension
            stem = PurePath(post["file"]["path"]).stem
            attachments.append(
                {
                    "server": get_fileserver_for_value(f"/data{post["file"]["path"]}"),
                    "name": post["file"].get("name"),
                    "extension": file_extension,
                    "name_extension": name_extension,
                    "stem": stem,
                    "path": post["file"]["path"],
                }
            )

    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",
                    "server": get_fileserver_for_value(f"/data{attachment["path"]}"),
                    "name": attachment["name"],
                    "path": attachment["path"],
                }
            )
        else:
            file_extension = PurePath(attachment["path"]).suffix
            name_extension = PurePath(attachment.get("name") or "").suffix
            # filename without extension
            stem = PurePath(attachment["path"]).stem
            attachments.append(
                {
                    "server": get_fileserver_for_value(f"/data{attachment["path"]}"),
                    "name": attachment.get("name"),
                    "extension": file_extension,
                    "name_extension": name_extension,
                    "stem": stem,
                    "path": attachment["path"],
                }
            )

    for i, attachment in enumerate(attachments):
        if attachment["extension"] in video_extensions:
            videos.append(
                {
                    "index": i,
                    "path": attachment["path"],
                    "name": attachment.get("name"),
                    "extension": attachment["extension"],
                    "name_extension": attachment["name_extension"],
                    "server": get_fileserver_for_value(f"/data{attachment["path"]}"),
                }
            )

    if post.get("poll") is not None:
        post["poll"]["total_votes"] = sum(choice["votes"] for choice in post["poll"]["choices"])
        post["poll"]["created_at"] = datetime.datetime.fromisoformat(post["poll"]["created_at"])
        if post["poll"]["closes_at"]:
            post["poll"]["closes_at"] = datetime.datetime.fromisoformat(post["poll"]["closes_at"])

    if (captions := post.get("captions")) is not None:
        for file_hash, caption_data in captions.items():
            for preview_data in [preview for preview in previews if preview.get("path") == file_hash]:
                if isinstance(caption_data, dict):
                    preview_data["caption"] = caption_data.get("text") or ""
                elif isinstance(caption_data, list):
                    preview_data["caption"] = " ".join(each.get("text") or "" for each in caption_data)
            for video in [video for video in videos if video["path"] == file_hash]:
                if isinstance(caption_data, dict):
                    video["caption"] = caption_data.get("text") or ""
                elif isinstance(caption_data, list):
                    video["caption"] = " ".join(each.get("text") or "" for each in caption_data)

    props = TDPostProps(
        flagged=is_post_flagged(service, artist_id, post_id),
        revisions=get_post_revisions(service, artist_id, post_id),
    )
    real_post = post if not post.get("revision_id") else get_post(service, artist_id, post_id)
    all_revisions = [real_post] + props["revisions"]
    for set_prev_next_revision in (post, *props["revisions"]):
        set_prev_next_revision["prev"] = real_post["prev"]
        set_prev_next_revision["next"] = real_post["next"]

    if props["revisions"]:
        last_date = real_post["added"]
        for i, rev in enumerate(all_revisions[:-1]):
            rev["added"] = all_revisions[i + 1]["added"]
        props["revisions"][-1]["added"] = last_date

        if real_post["service"] == "fanbox":
            top_rev_stripped = all_revisions[0].copy()
            top_rev_stripped.pop("file")
            top_rev_stripped.pop("added")
            top_rev_stripped.pop("revision_id", None)
            for fanbox_attachment in top_rev_stripped["attachments"]:
                if 41 >= len(fanbox_attachment["name"]) >= 39:
                    fanbox_attachment.pop("name", None)
            for duplicated_check_rev in all_revisions[1:]:
                duplicated_check_rev_file_stripped = duplicated_check_rev.copy()
                duplicated_check_rev_file_stripped.pop("file")
                duplicated_check_rev_file_stripped.pop("added")
                duplicated_check_rev_file_stripped.pop("revision_id", None)
                for fanbox_attachment in duplicated_check_rev_file_stripped["attachments"]:
                    if 41 >= len(fanbox_attachment["name"]) >= 39:
                        fanbox_attachment.pop("name", None)
                if duplicated_check_rev_file_stripped == top_rev_stripped:
                    all_revisions.remove(duplicated_check_rev)
                else:
                    top_rev_stripped = duplicated_check_rev_file_stripped

    if isinstance(post["tags"], str):
        post["tags"] = [tag.strip('"') for tag in post["tags"][1:-1].split(",")]

    transformed_revisions = list(reversed([
        (i, rev)
        for i, rev
        in enumerate(reversed(all_revisions))
    ]))
    props["revisions"] = transformed_revisions
    if post["service"] == "fanbox":
        post["content"] = DOWNLOAD_URL_FANBOX_REGEX.sub("", post["content"])
    post["content"] = sanitize_html(post["content"], allow_iframe=post["service"] == "fanbox")
    if post["service"] == "boosty":
        post["content"] = patch_inline_img(post["content"])

    return attachments, previews, videos, props
