import random
from typing import TypedDict, Optional, Literal
from flask import jsonify, make_response, redirect, url_for, request, session, abort

from src.internals.database.database import query_db
from src.lib.announcements import get_artist_announcements
from src.lib.artist import (
    get_artist,
    get_fancards_by_artist,
    get_linked_creators,
    get_artists_by_update_time,
    delete_creator_link,
    create_unapproved_link_request,
    get_random_artist_keys,
    TDArtist
)
from src.lib.filehaus import get_artist_share_count, get_artist_shares
from src.lib.post import get_artist_post_count, get_artist_posts_summary, get_all_posts_by_artist, get_render_data_for_posts, get_fileserver_for_value
from src.lib.posts import get_all_tags, count_all_posts_for_tag, get_tagged_posts
from src.lib.dms import count_user_dms, get_artist_dms
from src.lib.api import create_client_error_response, create_not_found_error_response
from src.utils.utils import parse_int, positive_or_none, step_int, take, sort_dict_list_by, offset_list
from src.utils.decorators import require_login
from src.pages.artists_types import ArtistShareProps, ArtistDisplayData, ArtistDMsProps, LinkedAccountsProps
from src.types.account.account import Account
from src.types.paysites import Paysite, Paysites

from src.pages.api.v1 import v1api_bp


@v1api_bp.get("/artists/updated")
def updated():
    base = dict(commit=True, sort_by="updated")
    limit = 50

    results = get_artists_by_update_time(offset=0, limit=limit)
    props = dict(
        currentPage="artists",
        display="cached updated artists",
        count=len(results),
        limit=limit,
    )

    response = make_response(jsonify(props=props, results=results, base=base), 200)
    response.headers["Cache-Control"] = "max-age=60, public, stale-while-revalidate=2592000"

    return response


@v1api_bp.get("/artists/random")
def random_artist():
    """todo decide after random posts with redis list if its worth"""
    artist = get_random_artist()

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

        return response

    response = make_response(jsonify(service=artist["service"], artist_id=artist["id"]), 200)

    return response


def get_random_artist():
    artists = get_random_artist_keys(1000)
    if len(artists) == 0:
        return None
    return random.choice(artists)


@v1api_bp.get("/creators")
def all_creators():
    """this view must be cached at nginx/cdn level"""
    query = """
        SELECT
            l.id,
            l.name,
            l.service,
            EXTRACT(epoch from l.indexed)::int AS indexed,
            EXTRACT(epoch from l.updated)::int AS updated,
            COALESCE(aaf.favorite_count, 0) AS favorited
        FROM lookup l
        LEFT JOIN (
            SELECT *
            FROM favorite_counts
        ) aaf ON
            l.id = aaf.artist_id
            AND l.service = aaf.service
        WHERE
            l.id NOT IN (SELECT id from dnp)
            AND (l.id, l.service) NOT IN (
               SELECT id, service FROM lookup l WHERE NOT EXISTS (SELECT FROM posts WHERE service = l.service AND "user" = l.id) AND service != 'discord');
    """
    return make_response(jsonify(query_db(query)), 200)


@v1api_bp.get("/<service>/user/<creator_id>/profile")
def get_creator(service, creator_id):
    artist = get_artist(service, creator_id)
    if not artist:
        response = make_response(jsonify({"error": "Creator not found."}), 404)
        response.headers["Cache-Control"] = "s-maxage=60"
        return response

    response = make_response(jsonify(artist), 200)
    response.headers["Cache-Control"] = "s-maxage=60"
    return response


class TDProfileDisplayData(TypedDict):
    service: str
    href: str


class TDProfilePostsProps(TypedDict):
    currentPage: Literal["posts"]
    id: str
    service: str
    name: str
    count: int
    limit: int
    artist: TDArtist
    display_data: TDProfileDisplayData
    dm_count: int
    share_count: int
    has_links: str


@v1api_bp.get("/<service>/user/<creator_id>/posts-legacy")
def get_profile_posts_legacy(service: str, creator_id: str):
    if not service:
        return create_client_error_response("Service name is required.")

    if not creator_id:
        return create_client_error_response("Profile ID is required.")

    if service == "discord":
        return create_client_error_response("Discord servers not allowed.")

    artist = get_artist(service, creator_id)

    if not artist:
        return create_not_found_error_response()

    if artist["public_id"] == creator_id and artist["id"] != creator_id:
        return create_client_error_response("Something something profile ID mismatch.")

    query = request.args.get("q", default="").strip()
    tags = sorted(request.args.getlist("tag"))
    limit = 50
    offset = positive_or_none(step_int(parse_int(request.args.get("o"), 0), limit))

    if offset is None:
        return create_client_error_response(f"Offset is not a multiple of {limit}.")

    if tags:
        posts = get_tagged_posts(tags, offset, limit, service, creator_id)
        total_count = count_all_posts_for_tag(tags, service, creator_id)
    elif not query or len(query) < 2:
        total_count = get_artist_post_count(service, creator_id)

        if offset > total_count:
            return create_client_error_response(f"Offset {offset} is bigger than total count {total_count}.")
        else:
            posts = get_artist_posts_summary(creator_id, service, offset, limit, "published DESC NULLS LAST")
    else:
        (posts, total_count) = do_artist_post_search(creator_id, service, query, offset, limit)

    (
        result_previews,
        result_attachments,
        result_is_image,
    ) = get_render_data_for_posts(posts)

    base = request.args.to_dict()
    base.pop("o", None)
    base["service"] = service
    base["artist_id"] = creator_id

    props = TDProfilePostsProps(
        currentPage="posts",
        id=creator_id,
        service=service,
        name=artist["name"],
        count=total_count,
        limit=limit,
        artist=artist,
        display_data=make_artist_display_data(artist),
        dm_count=count_user_dms(service, creator_id),
        share_count=get_artist_share_count(service, creator_id),
        has_links="✔️" if artist["relation_id"] else "0",
    )

    response = make_response(
        jsonify(
            props=props,
            base=base,
            results=posts,
            result_previews=result_previews,
            result_attachments=result_attachments,
            result_is_image=result_is_image,
            disable_service_icons=True,
        ),
        200,
    )
    response.headers["Cache-Control"] = "s-maxage=60"

    return response


@v1api_bp.get("/<service>/user/<creator_id>/announcements")
def get_announcements(service, creator_id):
    artist = get_artist(service, creator_id)
    if not artist:
        response = make_response(jsonify({"error": "Artist not found."}), 404)
        response.headers["Cache-Control"] = "s-maxage=600"
        return response
    announcements = get_artist_announcements(service, creator_id, reload=True)
    response = make_response(jsonify(announcements), 200)
    response.headers["Cache-Control"] = "s-maxage=600"
    return response


@v1api_bp.get("/<service>/user/<creator_id>/fancards")
def get_fancards(service, creator_id):
    artist = get_artist(service, creator_id)

    if not artist:
        response = make_response(jsonify({"error": "Artist not found."}), 404)
        response.headers["Cache-Control"] = "s-maxage=600"

        return response

    fancards = get_fancards_by_artist(creator_id, reload=True)

    for fancard in fancards:
        fhash = fancard["hash"]
        ext = fancard["ext"]
        fancard["path"] = f"/data/{fhash[0:2]}/{fhash[2:4]}/{fhash}{ext}"
        fancard["server"] = get_fileserver_for_value(fancard["path"])

    response = make_response(jsonify(fancards), 200)
    response.headers["Cache-Control"] = "s-maxage=600"

    return response


@v1api_bp.get("/<service>/user/<creator_id>/links")
def get_linked_accounts(service, creator_id):
    links = get_linked_creators(service, creator_id)

    response = make_response(jsonify(links), 200)
    response.headers["Cache-Control"] = "s-maxage=600"
    return response


@v1api_bp.delete("/<service>/user/<creator_id>/links")
@require_login
def delete_linked_account(service: str, creator_id: str, user: Account):
    if user.role != "administrator":
        abort(404)
    else:
        delete_creator_link(service, creator_id)
        return "", 204


@v1api_bp.get("/<service>/user/<artist_id>/links/new")
@require_login
def get_new_link_page(service: str, artist_id: str, user: Account):
    artist = get_artist(service, artist_id)
    if not artist:
        response = make_response(jsonify({"error": "Artist not found."}), 404)
        response.headers["Cache-Control"] = "s-maxage=600"

        return response
    elif artist["public_id"] == artist_id and artist["id"] != artist_id:
        return redirect(url_for(".get_new_link_page", service=service, artist_id=artist["id"]), code=301)

    base = dict(service=service, artist_id=artist_id)
    props = LinkedAccountsProps(
        id=artist_id,
        service=service,
        artist=artist,
        share_count=get_artist_share_count(service, artist_id),
        dm_count=count_user_dms(service, artist_id),
        has_links="✔️" if artist["relation_id"] else "0",
        display_data=make_artist_display_data(artist),
    )

    response = make_response(
        jsonify(
            props=props,
            base=base,
        ),
        200,
    )
    response.headers["Cache-Control"] = "s-maxage=600"

    return response


class TDArtistLinkRequest(TypedDict):
    service: str
    artist_id: str
    reason: Optional[str]


@v1api_bp.post("/<service>/user/<artist_id>/links/new")
@require_login
def post_new_link_page(service: str, artist_id: str, user: Account):
    body: TDArtistLinkRequest = request.get_json()

    dest_service = body["service"]
    dest_artist_id = body["artist_id"]
    reason = body["reason"] or ""

    from_artist = get_artist(service, artist_id)
    to_artist = get_artist(dest_service, dest_artist_id)

    if not from_artist:
        response = make_response(jsonify({"error": "Artist not found."}), 404)
        response.headers["Cache-Control"] = "s-maxage=600"

        return response
    elif from_artist["public_id"] == artist_id and from_artist["id"] != artist_id:
        return redirect(url_for(".post_new_link_page", service=service, artist_id=from_artist["id"]), code=301)

    base = dict(service=service, artist_id=artist_id)
    props = LinkedAccountsProps(
        id=artist_id,
        service=service,
        artist=from_artist,
        share_count=get_artist_share_count(service, artist_id),
        dm_count=count_user_dms(service, artist_id),
        has_links="✔️" if from_artist["relation_id"] else "0",
        display_data=make_artist_display_data(from_artist),
    )

    if not to_artist:
        message = f"Invalid creator (svc: {dest_service}, id: {dest_artist_id})"
        response = make_response(jsonify(error=message), 400)

        return response

    if len(reason) > 140:
        message = "Reason is too long"
        response = make_response(jsonify(error=message), 400)

        return response

    if dest_service == service and dest_artist_id == artist_id:
        message = "Can't link an artist to themself"
        response = make_response(jsonify(error=message), 400)

        return response

    if from_artist["relation_id"] == to_artist["relation_id"] and from_artist["relation_id"] is not None:
        message = "Already linked"
        response = make_response(jsonify(error=message), 400)

        return response

    create_unapproved_link_request(from_artist, to_artist, user.id, reason)

    response = make_response(
        jsonify(
            message="Request created. It will be shown here when approved.",
            props=props,
            base=base,
        ),
        200,
    )

    return response


@v1api_bp.get("/<service>/user/<artist_id>/tags")
def get_tags(service: str, artist_id: str):
    artist = get_artist(service, artist_id)
    if not artist:
        response = make_response(jsonify({"error": "Artist not found."}), 404)
        response.headers["Cache-Control"] = "s-maxage=600"

        return response
    elif artist["public_id"] == artist_id and artist["id"] != artist_id:
        return redirect(url_for(".get_tags", service=service, artist_id=artist["id"]), code=301)

    tags = get_all_tags(service, artist_id)
    props = dict(
        display_data=make_artist_display_data(artist),
        artist=artist,
        service=service,
        id=artist["id"],
        share_count=get_artist_share_count(service, artist_id),
        dm_count=count_user_dms(service, artist_id),
        has_links="✔️" if artist["relation_id"] else "0",
    )
    response = make_response(
        jsonify(
            props=props,
            tags=tags,
            service=service,
            artist=artist,
        ),
        200,
    )
    response.headers["Cache-Control"] = "s-maxage=600"

    return response


@v1api_bp.route("/<service>/user/<artist_id>/shares")
def get_shares(service: str, artist_id: str):
    base = request.args.to_dict()
    base.pop("o", None)
    base["service"] = service
    base["artist_id"] = artist_id

    dm_count = count_user_dms(service, artist_id)
    shares = get_artist_shares(artist_id, service)

    artist = get_artist(service, artist_id)
    if artist is None:
        response = make_response(jsonify({"error": "Artist not found."}), 404)
        response.headers["Cache-Control"] = "s-maxage=600"

        return response
    elif artist["public_id"] == artist_id and artist["id"] != artist_id:
        return redirect(url_for(".get_shares", service=service, artist_id=artist["id"]), code=301)

    props = ArtistShareProps(
        display_data=make_artist_display_data(artist),
        service=service,
        artist=artist,
        id=artist_id,
        dm_count=dm_count,
        share_count=len(shares),
        has_links="✔️" if artist["relation_id"] else "0",
    )

    response = make_response(
        jsonify(results=shares, props=props, base=base),
        200,
    )
    response.headers["Cache-Control"] = "s-maxage=60"

    return response


@v1api_bp.route("/<service>/user/<artist_id>/dms")
def get_dms(service: str, artist_id: str):

    artist = get_artist(service, artist_id)

    if artist is None:
        response = make_response(jsonify({"error": "Artist not found."}), 404)
        response.headers["Cache-Control"] = "s-maxage=600"

        return response
    elif artist["public_id"] == artist_id and artist["id"] != artist_id:
        return redirect(url_for(".get_dms", service=service, artist_id=artist["id"]), code=301)

    dms = get_artist_dms(service, artist_id)

    props = ArtistDMsProps(
        id=artist_id,
        service=service,
        artist=artist,
        display_data=make_artist_display_data(artist),
        share_count=get_artist_share_count(service, artist_id),
        dm_count=len(dms),
        dms=dms,
        has_links="✔️" if artist["relation_id"] else "0",
    )

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

    return response


def make_artist_display_data(artist: dict) -> ArtistDisplayData:
    service_name: str = artist["service"]
    pay_site: Paysite | None = getattr(Paysites, service_name, None)

    if pay_site:
        return ArtistDisplayData(service=pay_site.title, href=pay_site.user.profile(artist))

    raise Exception("Service not found in Paysites")


def do_artist_post_search(artist_id: str, service: str, search: str, o: int, limit: int):
    posts = get_all_posts_by_artist(artist_id, service)
    search = search.lower()

    matches = []
    for post in posts:
        if (
            search in post["content"].lower()
            or search in post["title"].lower()
            or search in " ".join(post["tags"] or []).lower()
        ):
            matches.append(post)

    matches = sort_dict_list_by(matches, "published", True)

    return take(limit, offset_list(o, matches)), len(matches)
