"""
Handle matching data in a FileNameParts (likely generated by a namer_file_parser.py) to
look up metadata (actors, studio, creation data, posters, etc) from the porndb.
"""

import argparse
import itertools
import json
import pathlib
import re
import sys
from datetime import date, timedelta
from pathlib import Path
from types import SimpleNamespace
from typing import List, Optional, Tuple
from urllib.parse import quote

import rapidfuzz
import requests
from loguru import logger
from unidecode import unidecode

from namer.filenameparser import parse_file_name
from namer.types import ComparisonResult, default_config, FileNameParts, LookedUpFileInfo, NamerConfig, Performer, set_permissions


def __find_best_match(query: Optional[str], match_terms: List[str], config: NamerConfig) -> Tuple[str, float]:
    powerset_iter = []
    max_size = min(len(match_terms), config.max_performer_names)
    for length in range(1, max_size + 1):
        data = map(" ".join, itertools.combinations(match_terms, length))
        powerset_iter = itertools.chain(powerset_iter, data)
    ratio = rapidfuzz.process.extractOne(query, choices=powerset_iter)
    return ratio[0], ratio[1] if ratio is not None else ratio


def __attempt_better_match(existing: Tuple[str, float],
                           query: Optional[str],
                           match_terms: List[str],
                           namer_config: NamerConfig) -> Tuple[str, float]:
    if existing is not None and existing[1] >= 89.9:
        return existing
    found = __find_best_match(query, match_terms, namer_config)
    if existing is None:
        return found
    if found is None:
        return "", 0.0
    return existing if existing[1] >= found[1] else found


def __evaluate_match(name_parts: FileNameParts, looked_up: LookedUpFileInfo, namer_config: NamerConfig) -> ComparisonResult:
    site = False
    found_site = None
    if looked_up.site is not None:
        found_site = re.sub(r"[\- .+_]", "", looked_up.site).upper()
        if name_parts.site is not None:
            site = name_parts.site is None or re.sub(r"[\- .+_]", "", name_parts.site.upper()) in found_site or unidecode(re.sub(r"[\- .+_]", "", name_parts.site.upper())) in found_site
    release_date = False
    if found_site in namer_config.sites_with_no_date_info:
        release_date = True
    else:
        release_date = name_parts.date is not None and (name_parts.date == looked_up.date or unidecode(name_parts.date) == looked_up.date)

    result: Tuple[str, float] = ('', 0.0)

    # Full Name
    all_performers = list(map(lambda p: p.name, looked_up.performers))
    if looked_up.name is not None:
        all_performers.insert(0, looked_up.name)

    result = __attempt_better_match(result, name_parts.name, all_performers, namer_config)
    if name_parts.name is not None:
        result = __attempt_better_match(result, unidecode(name_parts.name), all_performers, namer_config)

    # First Name Powerset.
    if result is not None and result[1] < 89.9:
        all_performers = list(map(lambda p: p.name.split(" ")[0], looked_up.performers))
        if looked_up.name is not None:
            all_performers.insert(0, looked_up.name)
        result = __attempt_better_match(result, name_parts.name, all_performers, namer_config)
        if name_parts.name is not None:
            result = __attempt_better_match(result, unidecode(name_parts.name), all_performers, namer_config)

    return ComparisonResult(
        name=result[0],
        name_match=result[1],
        datematch=release_date,
        sitematch=site,
        name_parts=name_parts,
        looked_up=looked_up,
    )


def __update_results(results: List[ComparisonResult],
                     name_parts: FileNameParts,
                     namer_config: NamerConfig,
                     skipdate: bool = False,
                     skipname: bool = False):
    if len(results) == 0 or not results[0].is_match():
        for match_attempt in __get_metadataapi_net_fileinfo(name_parts, namer_config, skipdate, skipname):
            result = __evaluate_match(name_parts, match_attempt, namer_config)
            results.append(result)
        results = sorted(results, key=__match_percent, reverse=True)


def __metadata_api_lookup(name_parts: FileNameParts, namer_config: NamerConfig) -> List[ComparisonResult]:
    results = []
    __update_results(results, name_parts, namer_config)
    __update_results(results, name_parts, namer_config, skipdate=True)
    __update_results(results, name_parts, namer_config, skipdate=True, skipname=True)
    __update_results(results, name_parts, namer_config, skipname=True)

    if name_parts.date is not None and (len(results) == 0 or not results[-1].is_match()):
        name_parts.date = (date.fromisoformat(name_parts.date) + timedelta(days=-1)).isoformat()
        logger.info("Not found, trying 1 day before: {}", name_parts)
        __update_results(results, name_parts, namer_config)
        __update_results(results, name_parts, namer_config, skipdate=False, skipname=True)

    if name_parts.date is not None and (len(results) == 0 or not results[-1].is_match()):
        name_parts.date = (date.fromisoformat(name_parts.date) + timedelta(days=2)).isoformat()
        logger.info("Not found, trying 1 day after: {}", name_parts)
        __update_results(results, name_parts, namer_config)
        __update_results(results, name_parts, namer_config, skipdate=False, skipname=True)
    return results


def __match_percent(result: ComparisonResult) -> float:
    addvalue = 0.00
    if result.is_match() is True:
        addvalue = 1000.00
    value = (result.name_match + addvalue) if result is not None and result.name_match is not None else addvalue
    logger.info("Name match was {:.2f} for {}", value, result.name)
    return value


@logger.catch
def __get_response_json_object(url: str, authtoken: str) -> str:
    """
    returns json object with info
    """
    headers = {
        "Authorization": f"Bearer {authtoken}",
        "Content-Type": "application/json",
        "Accept": "application/json",
        "User-Agent": "namer-1",
    }
    with requests.get(url, headers=headers) as response:
        response.raise_for_status()
        return response.text


@logger.catch
def get_image(url: Optional[str], infix: str, video_file: Optional[Path], config: NamerConfig) -> Optional[Path]:
    """
    returns json object with info
    """
    if url is not None and video_file is not None:
        file = video_file.parent / (video_file.stem + infix + pathlib.Path(url).suffix)
        if config.enabled_poster and url.startswith("http") and not file.exists():
            headers = {
                "Authorization": f"Bearer {config.porndb_token}",
                "User-Agent": "namer-1",
            }
            file.parent.mkdir(parents=True, exist_ok=True)
            http = requests.get(url, headers=headers, stream=True)
            if http.ok:
                with open(file, 'wb') as io_wrapper:
                    for data in http.iter_content(1024):
                        io_wrapper.write(data)
                    set_permissions(file, config)
                    return file
            return None
        poster = (video_file.parent / url).resolve()
        return poster if poster.exists() and poster.is_file() else None
    return None


@logger.catch
def get_trailer(url: Optional[str], video_file: Optional[Path], namer_config: NamerConfig) -> Optional[Path]:
    """
    returns json object with info
    """
    if namer_config.trailer_location is not None and not len(namer_config.trailer_location) == 0 and url is not None and len(url) > 0 and video_file is not None:
        logger.info("Attempting to downlaod trailer: {}", url)
        location = namer_config.trailer_location[:max([idx for idx, x in enumerate(namer_config.trailer_location) if x == "."])]
        urlparts = url.split("?")[0].split(".")
        ext = "mp4"
        if urlparts is not None and len(urlparts) > 0 and urlparts[-1].lower() in namer_config.target_extensions:
            ext = urlparts[-1]
        trailerfile: Path = video_file.parent / (location + "." + ext)
        trailerfile.parent.mkdir(parents=True, exist_ok=True)
        if not trailerfile.exists() and url.startswith("http"):
            headers = {"User-Agent": "namer-1"}
            if "metadataapi.net" in url:
                headers["Authorization"] = f"Bearer {namer_config.porndb_token}"
            http = requests.get(url, headers=headers, stream=True)
            if http.ok:
                with open(trailerfile, 'wb') as io_wrapper:
                    for data in http.iter_content(1024):
                        io_wrapper.write(data)
                    set_permissions(trailerfile, namer_config)
                    return trailerfile
            return None
        trailer = (video_file.parent / url).resolve()
        return trailer if trailer.exists() and trailer.is_file() else None
    return None


def __jsondata_to_fileinfo(data, url, json_response, name_parts) -> LookedUpFileInfo:
    file_info = LookedUpFileInfo()
    file_info.uuid = data._id  # pylint: disable=protected-access
    file_info.name = data.title
    file_info.description = data.description
    file_info.date = data.date
    file_info.source_url = data.url
    file_info.poster_url = data.poster
    file_info.trailer_url = data.trailer
    if data.background is not None:
        file_info.background_url = data.background.large
    file_info.site = data.site.name
    file_info.look_up_site_id = data._id  # pylint: disable=protected-access
    for json_performer in data.performers:
        performer = Performer()
        if hasattr(json_performer, "parent") and hasattr(json_performer.parent, "extras"):
            performer.role = json_performer.parent.extras.gender
        performer.name = json_performer.name
        performer.image = json_performer.image
        file_info.performers.append(performer)
    file_info.original_query = url
    file_info.origninal_response = json_response
    file_info.original_parsed_filename = name_parts
    tags = []
    if hasattr(data, "tags"):
        for tag in data.tags:
            tags.append(tag.name)
        file_info.tags = tags
    return file_info


def __metadataapi_response_to_data(json_object, url, json_response, name_parts) -> List[LookedUpFileInfo]:
    file_infos = []
    if hasattr(json_object, "data"):
        if isinstance(json_object.data, list):
            for data in json_object.data:
                found_file_info = __jsondata_to_fileinfo(data, url, json_response, name_parts)
                file_infos.append(found_file_info)
        else:
            found_file_info = __jsondata_to_fileinfo(json_object.data, url, json_response, name_parts)
            file_infos.append(found_file_info)
    return file_infos


def __build_url(site: Optional[str] = None, release_date: Optional[str] = None, name: Optional[str] = None, uuid: Optional[str] = None) -> str:
    if uuid is not None:
        query = "/" + str(uuid)
    else:
        query = "?parse="
        if site is not None:
            # There is a known issue in tpdb, where site names are not matched due to casing.
            # example Teens3Some fails, but Teens3some succeeds.  Turns out Teens3Some is treated as 'Teens 3 Some'
            # and Teens3some is treated correctly as 'Teens 3some'.  Also, 'brazzersextra' still match 'Brazzers Extra'
            # Hense, the hack of lower casing the site.
            query += quote(re.sub(r" ", ".", site.lower())) + "."
        if release_date is not None:
            query += release_date + "."
        if name is not None:
            query += quote(re.sub(r" ", ".", name))
        query += "&limit=25"
    return f"https://api.metadataapi.net/scenes{query}"


def __get_metadataapi_net_fileinfo(name_parts: FileNameParts, namer_config: NamerConfig, skipdate: bool, skipname: bool) -> List[LookedUpFileInfo]:
    release_date = name_parts.date if not skipdate else None
    name = name_parts.name if not skipname else None
    url = __build_url(name_parts.site, release_date, name)
    logger.info("Querying: {}", url)
    json_response = __get_response_json_object(url, namer_config.porndb_token)
    file_infos = []
    if json_response is not None and json_response.strip() != "":
        logger.debug("json_repsonse: \n{}", json_response)
        json_obj = json.loads(json_response, object_hook=lambda d: SimpleNamespace(**d))
        formatted = json.dumps(json.loads(json_response), indent=4, sort_keys=True)
        file_infos = __metadataapi_response_to_data(json_obj, url, formatted, name_parts)
    return file_infos


def __get_complete_metadatapi_net_fileinfo(name_parts: FileNameParts, uuid: str, namer_config: NamerConfig) -> Optional[LookedUpFileInfo]:
    url = __build_url(uuid=uuid)
    logger.info("Querying: {}", url)
    json_response = __get_response_json_object(url, namer_config.porndb_token)
    file_infos = []
    if json_response is not None and json_response.strip() != "":
        logger.debug("json_repsonse: \n{}", json_response)
        json_obj = json.loads(json_response, object_hook=lambda d: SimpleNamespace(**d))
        formatted = json.dumps(json.loads(json_response), indent=4, sort_keys=True)
        file_infos = __metadataapi_response_to_data(json_obj, url, formatted, name_parts)
    if len(file_infos) > 0:
        return file_infos[0]
    return None


def match(file_name_parts: FileNameParts, namer_config: NamerConfig) -> List[ComparisonResult]:
    """
    Give parsed file name parts, and a porndb token, returns a sorted list of possible matches.
    Matches will appear first.
    """
    comparison_results = __metadata_api_lookup(file_name_parts, namer_config)
    comparison_results = sorted(comparison_results, key=__match_percent, reverse=True)
    # Works around the porndb not returning all info on search queries by looking up the full data
    # with the uuid of the best match.
    if len(comparison_results) > 0 and comparison_results[0].is_match() is True:
        uuid = comparison_results[0].looked_up.uuid
        if uuid is not None:
            file_infos = __get_complete_metadatapi_net_fileinfo(file_name_parts, uuid, namer_config)
            if file_infos is not None:
                comparison_results[0].looked_up = file_infos
    return comparison_results


def main(argslist: List[str]):
    """
    Looks up metadata from metadataapi.net base on file name.
    """
    description = """
    Command line interface to look up a suggested name for an adult movie file based on an input string
    that is parsable by namer_file_parser.py
    """
    parser = argparse.ArgumentParser(description=description)
    parser.add_argument("-c", "--configfile", help="override location for a configuraion file.", type=pathlib.Path)
    parser.add_argument("-f", "--file", help="File we want to provide a match name for.", required=True, type=pathlib.Path)
    parser.add_argument("-j", "--jsonfile", help="write returned json to this file.", type=pathlib.Path)
    parser.add_argument("-v", "--verbose", help="verbose, print logs", action="store_true")
    args = parser.parse_args(args=argslist)
    level = "DEBUG" if args.verbose else "ERROR"
    logger.remove()
    logger.add(sys.stdout, format="{time} {level} {message}", level=level)
    config = default_config()
    file_name = parse_file_name(args.file.name, config.name_parser)
    match_results = match(file_name, config)
    if len(match_results) > 0 and match_results[0].is_match() is True:
        print(match_results[0].looked_up.new_file_name(config.inplace_name))
        if args.jsonfile is not None and match_results[0].looked_up is not None and match_results[0].looked_up.origninal_response is not None:
            Path(args.jsonfile).write_text(match_results[0].looked_up.origninal_response, encoding="UTF-8")


if __name__ == "__main__":
    main(sys.argv[1:])
