#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Rewriting of the download manager script
with the intention to make it
more modular with the use of flags
in order to avoid unnecesary modifications
to the cofig files.
Also following in line more posix and python rules.
"""

import re
from pathlib import Path
import argparse
import yaml
import db
from typing import Dict
from functions import LOG
from functions import run
from functions import list_lines
from functions import load_config_variables
from functions import parse_link
from argparser import argparser
from classes.user import User
from classes.gallery import Gallery

# GLOBAL VARIABLE SECTION
CONFIGS = None
# Enable a default "everyone" flag for when running stuff like download gallery
USERS = []
ARGS = None


def init_globals() -> None:
    """Initialize global config and CLI args."""
    global CONFIGS, USERS, ARGS
    if CONFIGS is None:
        CONFIGS = load_config_variables()
        USERS = ["everyone"] + [user["name"] for user in CONFIGS["users"]]
        ARGS = argparser(USERS)


def get_args() -> argparse.Namespace:
    """Return initialized CLI args."""
    init_globals()
    assert ARGS is not None
    return ARGS


def get_configs() -> dict:
    """Return initialized config."""
    init_globals()
    assert CONFIGS is not None
    return CONFIGS


class Video:
    """Just a simple class to unify the Video parameters into a single one."""

    def __init__(self) -> None:
        self.use_archive: bool = True
        self.link: str = ""
        self.dest: str = ""
        self.database: str = ""


def get_index(name: str) -> int:
    """Find the index in the config file"""
    configs = get_configs()
    return next((i for i, d in enumerate(configs["users"]) if d["name"] == name), -1)


def parse_gallery(gdl_list: str, user: User) -> None:
    """Processes the gallery-dl command based on the selected gallery"""
    args = get_args()
    list_path = user.lists[gdl_list]
    with open(list_path, "r", encoding="utf-8") as r_file:
        links = list(map(lambda x: x.rstrip(), r_file))
    for link in filter(None, links):
        gallery = Gallery()
        gallery.archive = args.flag_archive
        gallery.skip_arg = " -o skip=true" if not args.flag_skip else ""
        gallery.dest = "download"
        gallery.link = link
        gallery.opt_args = parse_instagram(link)

        gallery.generate_command(user)
        handler = _make_gallery_error_handler(link)
        gallery.run_command(args.flag_verbose, on_line=handler, log_failure=False)


def parse_instagram(link: str, post_type: list[str] | str | None = None) -> list[str]:
    """Fix instagram links"""
    args = get_args()
    if "instagram" not in link:
        return []
    use_type = args.post_type if post_type is None else post_type
    if isinstance(use_type, list):
        return ["-o", f"include={','.join(use_type)}"]
    return ["-o", f"include={use_type}"]


REVISION_ERRORS = {
    "NotFoundError: Requested user could not be found",
    "Unable to retrieve Tweets from this timeline",
    "No results for",
}

TRANSIENT_ERRORS = {
    "User input required (password)",
    "429",
    "rate limit",
    "timed out",
    "timeout",
    "Network",
    "connection",
}


def _make_gallery_error_handler(link: str):
    norm = db.normalize_url(link)

    def handle(line: str) -> None:
        if "[error]" in line:
            reason = line.split("[error]", 1)[1].strip()
            LOG.warning("Error for %s: %s", link, reason)
            if reason in REVISION_ERRORS:
                with db.connect() as conn:
                    db.mark_requires_revision_by_norm(conn, norm, reason)
                    conn.commit()
                LOG.warning("Marked requires_revision for %s", link)
            if any(tok in reason for tok in TRANSIENT_ERRORS):
                LOG.warning("Transient error for %s: %s", link, reason)
            return
        if "No results for" in line:
            with db.connect() as conn:
                db.mark_requires_revision_by_norm(conn, norm, "No results for")
                conn.commit()
            LOG.warning("Marked requires_revision for %s", link)
            return

    return handle


def _comic_skip_arg(link: str, flag_skip: bool) -> str:
    if not flag_skip:
        return ""
    if re.search(r"readcomiconline", link):
        return " --chapter-range 1"
    if re.search(r"manganato|mangahere|webtoons", link):
        return " --chapter-range 1-5"
    return ""


def _handle_gallery_link(user: User, link: str, args, conn) -> None:
    add_res = db.add_link(conn, user.name, parse_link(link), assume_yes=True, source="push")
    row = add_res.get("row")
    if row and row["banned_at"]:
        LOG.warning("Link is banned, skipping: %s", link)
        return
    if row and not row["enabled"]:
        LOG.warning("Link is disabled, skipping: %s", link)
        return

    gallery = Gallery()
    gallery.archive = args.flag_archive
    gallery.skip_arg = " -o skip=true" if not args.flag_skip else ""
    gallery.link = parse_link(link)
    gallery.dest = "download"
    gallery.opt_args = parse_instagram(link)
    gallery.generate_command(user)
    handler = _make_gallery_error_handler(link)
    gallery.run_command(args.flag_verbose, on_line=handler, log_failure=False)


def _handle_comic_link(link: str, args) -> None:
    gallery = Gallery()
    gallery.archive = args.flag_archive
    gallery.skip_arg = _comic_skip_arg(link, args.flag_skip)
    gallery.link = link
    gallery.generate_command(is_comic=True)
    handler = _make_gallery_error_handler(link)
    gallery.run_command(args.flag_verbose, on_line=handler, log_failure=False)
    save_comic(link)


def _handle_video_link(user: User, link: str, args) -> None:
    video = Video()
    video.use_archive = args.flag_archive
    video.link = link
    video.dest = str(user.directories["media"])
    video.database = str(user.dbs["media"])
    run(video_command(video), args.flag_verbose)


def _handle_other_link(user: User, link: str, args) -> None:
    LOG.info("Other type of download %s", link)
    gallery = Gallery()
    gallery.archive = False
    gallery.skip_arg = " -o directory='[]'"
    gallery.link = link
    gallery.dest = "push"
    gallery.generate_command(user)
    handler = _make_gallery_error_handler(link)
    gallery.run_command(args.flag_verbose, on_line=handler, log_failure=False)


def video_command(video: Video):
    """Filters and processes the required command to download videos"""
    command = ["yt-dlp"]
    rgx_yt = re.compile(r"(https:\/\/youtube|https:\/\/www.youtube|https:\/\/youtu.be)")
    rgx_music = re.compile(r"(https:\/\/music.youtube.*)")

    if re.search(r"chaturbate", video.link):
        return ["stream-dl", video.link.rstrip("/").split("/")[-1]]

    if rgx_yt.search(video.link):
        command += [
            "--embed-subs",
            "--embed-thumbnail",
            "--embed-metadata",
            "--embed-chapters",
            "-o",
            f"{video.dest}/%(title)s.%(ext)s",
        ]

    elif rgx_music.search(video.link):
        if video.use_archive:
            command += ["--download-archive", video.database]
        command += [
            "--no-playlist",
            "--newline",
            "-x",
            "--audio-format",
            "best",
            "--add-metadata",
            "--audio-quality",
            "0",
            "-o",
            f"{video.dest}/%(title)s.%(ext)s",
        ]

    else:  # Any other video link, just do it generic
        command += ["-f", "mp4", "-o", f"{video.dest}/%(title)s.%(ext)s"]

    LOG.info("%s %s", " ".join(command), video.link)
    return command + [video.link]


def comic_manager(skip_arg: str, category: str) -> None:
    """Process the information to download manga"""
    args = get_args()
    configs = get_configs()
    re_cat = "manga|webtoon" if category == "manga" else "readcomiconline"
    with open(configs["comic"]["comic-list"], "r", encoding="utf-8") as r_file:
        links = list(filter(lambda x: re.search(re_cat, x), r_file))

    for link in links:
        gallery = Gallery()
        gallery.archive = args.flag_archive
        gallery.skip_arg = skip_arg
        gallery.link = link
        gallery.generate_command(is_comic=True)
        gallery.run_command(args.flag_verbose)


def print_webcomics(webcomics: Dict[str, Dict]) -> int:
    """Prints a list of webcomics, and returns an index."""
    for index, entry in enumerate(webcomics["webcomics"]):
        print(list_lines(index, entry["name"]))

    max_index = len(webcomics["webcomics"]) - 1
    while True:
        raw = input("Select a webcomic: ").strip()
        if not raw.isdigit():
            print("Please enter a number.")
            continue
        choice = int(raw)
        if 0 <= choice <= max_index:
            return choice
        print(f"Please enter a number between 0 and {max_index}.")


def webcomic_manager():
    """Process the information to download webcomics"""
    args = get_args()
    configs = get_configs()
    with open(configs["comic"]["webcomic-list"], "r", encoding="utf-8") as r_file:
        webcomics = yaml.safe_load(r_file)

    usr_input = print_webcomics(webcomics)

    # Determines where the webcomic will be downloaded
    rating = webcomics["webcomics"][usr_input]["type"]
    dest = webcomics["global"][f"{rating}_directory"]
    name = webcomics["webcomics"][usr_input]["name"]
    link = webcomics["webcomics"][usr_input]["url"]
    nxt_code = webcomics["webcomics"][usr_input]["next_code"]
    img_code = webcomics["webcomics"][usr_input]["image_code"]

    LOG.info("The webcomic is %s", dest)

    command = [
        "webcomix",
        "custom",
        name,
        "--start-url",
        link,
        f"--next-page-xpath={nxt_code}",
        f"--image-xpath={img_code}",
        "-y",
        "--cbz",
    ]

    run(command, args.flag_verbose, cwd=Path(dest))


def save_comic(link: str) -> None:
    """Add comic/manga link to the list"""
    configs = get_configs()
    list_comic = configs["comic"]["comic-list"]
    with open(list_comic, "r", encoding="utf-8") as r_file:
        links = r_file.read().lower()
    if parse_link(link).lower() in links:
        LOG.info("Graphic novel repeated, not saving")
        return
    LOG.info("New graphic novel, saving")

    with open(list_comic, "a", encoding="utf-8") as w_file:
        w_file.write(link + "\n")


def push_manager(user: User, links: list[str] | None = None) -> None:
    """Filters out the URL to use the appropiate downloader"""
    args = get_args()
    # Creates an array which will store any links that should use youtube-dl
    rgx_gallery = re.compile(
        r"(x\.com\/\w+((?=.*media)|(?!.*status)))"
        r"|(men\.wikifeet)"
        r"|(furaffinity\.net\/user\/)"
        r"|((deviantart\.com\/\w+(?!.*\/art\/)))"
        r"|(furaffinity\.net\/gallery\/)"
        r"|(furaffinity\.net\/scraps\/)"
        r"|(furaffinity\.net\/favorites\/)"
        r"|(instagram.com(?!\/p\/)\/\w+)"
        r"|(e621\.net((?=\/post\/)|(?!\/posts\/)))"
        r"|(flickr\.com\/photos\/\w+\/(?!\d+))"
        r"|(tumblr\.com(?!\/post\/))"
        r"|(kemono\.party\/(fanbox|gumroad|patreon)(?!\/user\/\d+\/post))"
        r"|(blogspot\.com(?!\/))"
        r"|(rule34\.paheal\.net\/post\/(?!view))"
        r"|(rule34\.xxx\/index\.php\?page\=post&s=(?!view))"
        r"|(pixiv\.net\/(en\/)?((?=users)|(?!artwork)))"
        r"|(fanbox\.cc\/@\w+(?!.*posts\/\d+))"
        r"|(reddit\.com\/(user|u))"
        r"|(baraag\.net\/((@\w+)|(?!\/\d+)))"
        r"|(pinterest\.com\/(?!pin\/\d+))"
        r"|(redgifs\.com\/(users|u|(?!watch)))"
        r"|(bsky\.app\/profile\/(?!.*\/post\/))"
    )
    rgx_video = re.compile("youtu.be|youtube|pornhub|xtube|xvideos|chaturbate")
    rgx_comic = re.compile("readcomiconline|mangahere|mangadex|webtoons|manganato")

    if links is None:
        with open(user.lists["push"], "r", encoding="utf-8") as r_file:
            links = list(map(lambda x: x.rstrip(), r_file))
    links_galleries = filter(rgx_gallery.search, links)
    links_videos = filter(rgx_video.search, links)
    links_comics = filter(rgx_comic.search, links)
    links_other = filter(
        lambda x: (not rgx_video.search(x))
        and (not rgx_gallery.search(x))
        and (not rgx_comic.search(x)),
        links,
    )

    with db.connect() as conn:
        for link in links_galleries:
            _handle_gallery_link(user, link, args, conn)

        conn.commit()

    for link in links_comics:
        _handle_comic_link(link, args)

    for link in links_videos:
        _handle_video_link(user, link, args)

    for link in links_other:
        _handle_other_link(user, link, args)

    # Flush the push list, cleans all the contents
    with open(user.lists["push"], "w", encoding="utf-8") as w_file:
        w_file.close()


def scrapper_manager(user: User) -> None:
    """Analyze the user arguments and call in functions"""
    args = get_args()
    user.list_manager()
    if re.search(r"main|instagram|kemono", args.scrapper):
        parse_gallery(args.scrapper, user)
    elif args.scrapper == "push":
        push_manager(user)
    elif re.search("^comic|manga", args.scrapper):
        skip_arg = " --chapter-range 1" if args.flag_skip else ""
        skip_arg += "-5" if args.scrapper == "manga" else ""
        comic_manager(skip_arg, args.scrapper)
    elif re.search("webcomic", args.scrapper):
        webcomic_manager()


def scrap_everyone() -> None:
    """Iterates over every user of my scrapper"""
    args = get_args()
    configs = get_configs()
    for current_user in configs["users"]:
        user = User(get_index(current_user["name"]))
        LOG.info("Scrapping %s for %s", args.scrapper, current_user["name"])
        scrapper_manager(user)


def main():
    """Main module to decide what to do based on the parsed arguments"""
    args = get_args()
    if args.scrapper:
        rgx_shared = re.compile("push|main|instagram|kemono")
        if (args.user == "everyone") and (rgx_shared.search(args.scrapper)):
            scrap_everyone()
        else:
            scrapper_manager(User(get_index(args.user)))
    elif args.link:
        is_admin = args.user in ("everyone", "jawz")
        user = User(get_index("jawz" if is_admin else args.user))
        links = [parse_link(lnk) for grp in args.link for lnk in grp]
        push_manager(user, links=links)


if __name__ == "__main__":
    main()
