use crate::{ cli::ARGUMENTS, http::CLIENT };
use anyhow::{ Context, Result, format_err };
use regex::{ Captures, Regex };
use serde::Deserialize;
use std::{
    collections::HashSet,
    fmt::{ self, Display, Formatter, Write },
    fs::File,
    io::{ BufRead, BufReader, Read },
    path::PathBuf,
    sync::LazyLock,
};
use strum_macros::{ Display, EnumString };

// favorite post implementation is redundant in that we turn posts into
// targets to get valid (archive) paths, then turn them back into posts.
#[derive(Deserialize, Debug)]
struct FavoritePost {
    id: String,
    user: String,
    service: Service,
    // file: Option<PostFileRaw>,
    // attachments: Vec<PostFileRaw>,
}

impl FavoritePost {
    fn into_target(self) -> Target {
        let (service, user) = (self.service, self.user);

        let (path, archive_path) = (
            make_pathbuf(service, &user),
            make_archive_pathbuf(service, &user),
        );

        Target::Creator {
            service,
            user,
            subtype: SubType::Post(self.id),
            path,
            archive_path,
        }
    }
}

pub async fn try_fetch_favorites() -> Result<Vec<Target>> {
    async fn try_fetch_artists(host: &str) -> Result<Vec<Info>> {
        Ok(
            CLIENT.get(format!("https://{host}/api/v1/account/favorites?type=artist"))
                .send().await?
                .json().await?
        )
    }
    async fn try_fetch_posts(host: &str) -> Result<Vec<FavoritePost>> {
        Ok(
            CLIENT.get(format!("https://{host}/api/v1/account/favorites?type=post"))
                .send().await?
                .json().await?
        )
    }

    let (mut artists, mut posts) = (Vec::new(), Vec::new());

    if ARGUMENTS.coomer_user.is_some() {
        artists.append(&mut try_fetch_artists("coomer.st").await?);
        posts.append(&mut try_fetch_posts("coomer.st").await?);
    }

    if ARGUMENTS.kemono_user.is_some() {
        artists.append(&mut try_fetch_artists("kemono.cr").await?);
        posts.append(&mut try_fetch_posts("kemono.cr").await?);
    }

    let mut targets = Vec::with_capacity(artists.len() + posts.len());

    for artist in artists {
        let (service, user) = (artist.service.parse()?, artist.id);
        let (path, archive_path) = (
            make_pathbuf(service, &user),
            make_archive_pathbuf(service, &user),
        );

        targets.push(Target::Creator {
            service,
            user,
            subtype: SubType::None,
            path,
            archive_path,
        });
    }

    for post in posts {
        targets.push(post.into_target());
    }

    Ok(targets)
}

pub enum Target {
    Creator {
        service: Service,
        user: String,
        subtype: SubType,
        path: PathBuf,
        archive_path: PathBuf,
    },
    Discord {
        server: String,
        channel: Option<String>,
        offset: Option<usize>,
        path: PathBuf,
        archive_path: PathBuf,
    },
}

impl Display for Target {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            Target::Creator { service, user, subtype, .. } => {
                let _ = write!(f, "{service}/{user}");

                if let SubType::Post(p) = subtype {
                    let _ = write!(f, "/{p}");
                }
            }
            Target::Discord { server, channel, .. } => {
                let _ = write!(f, "discord/{server}");

                if let Some(chan) = channel {
                    let _ = write!(f, "/{chan}");
                }
            }
        }

        Ok(())
    }
}

#[derive(PartialEq, Eq, Hash)]
pub enum SubType {
    PageOffset(usize),
    Post(String),
    None,
}

#[derive(Deserialize, Debug)]
struct Info {
    id: String, // "5564244",
    // name: String, // "theobrobine",
    service: String, // "patreon",
    // indexed: String, // "2020-09-30T06:13:38.348472",
    // updated: String, // "2025-05-30T14:07:16.596525",
    // public_id: Option<String>, // "theobrobine",
    // relation_id: Option<u64>, // 8,
    // has_chats: Option<bool>, // false
}

type LazyRegex = LazyLock<Regex>;

static RE_LINKED: LazyRegex = LazyLock::new(|| {
    Regex::new(
        r"^(?:https://)?(?:coomer\.(?:su|st|party)|kemono\.(?:su|cr|party))/(?<service>[a-z]+)/user/(?<user>[a-z|A-Z|0-9|\-|_|\.]+)/links$"
    ).unwrap()
});

static RE_CREATOR: LazyRegex = LazyLock::new(|| {
    Regex::new(
        r"^(?:https://)?(?:coomer\.(?:su|st|party)|kemono\.(?:su|cr|party))/(?<service>[a-z]+)/user/(?<user>[a-z|A-Z|0-9|\-|_|\.]+)$"
    ).unwrap()
});

static RE_PAGE: LazyRegex = LazyLock::new(|| {
    Regex::new(
        r"^(?:https://)?(?:coomer\.(?:su|st|party)|kemono\.(?:su|cr|party))/(?<service>[a-z]+)/user/(?<user>[a-z|A-Z|0-9|\-|_|\.]+)\?o=(?<offset>(0|50|[1-9]+(0|5)0))$"
    ).unwrap()
});

static RE_POST: LazyRegex = LazyLock::new(|| {
    Regex::new(
        r"^(?:https://)?(?:coomer\.(?:su|st|party)|kemono\.(?:su|cr|party))/(?<service>[a-z]+)/user/(?<user>[a-z|A-Z|0-9|\-|_|\.]+)/post/(?<post>[a-z|A-Z|0-9|\-|_|\.]+)$"
    ).unwrap()
});

static RE_DISCORD: LazyRegex = LazyLock::new(|| {
    Regex::new(
        r"^(?:https://)?kemono\.(?:su|cr|party)/discord/server/(?<server>[0-9]{17,19})(/(?<channel>[0-9]{17,19}))?$"
    ).unwrap()
});

static RE_DISCORD_PAGE: LazyRegex = LazyLock::new(|| {
    Regex::new(
        r"^(?:https://)?kemono\.(?:su|cr|party)/discord/server/(?<server>[0-9]{17,19})(/(?<channel>[0-9]{17,19}))?(\?o=(?<offset>\d+))?$"
    ).unwrap()
});

async fn try_fetch_linked_accounts(service: Service, user: &str) -> Result<Vec<Info>> {
    let host = service.host();
    let service = service.as_static_str();

    let mut url = String::with_capacity(8 + host.len() + 8 + service.len() + 6 + user.len() + 8);
    let _ = write!(url, "https://{host}/api/v1/{service}/user/{user}/profile");

    let account = CLIENT.get(url).send().await?.json().await?;
    let mut accounts = Vec::with_capacity(4);
    accounts.push(account);

    let mut linked_accounts_url = String::with_capacity(
        8 + host.len() + 8 + service.len() + 6 + user.len() + 6
    );
    let _ = write!(linked_accounts_url, "https://{host}/api/v1/{service}/user/{user}/links");

    let mut linked_accounts = CLIENT.get(linked_accounts_url).send().await?.json().await?;
    accounts.append(&mut linked_accounts);

    Ok(accounts)
}

impl Target {
    pub fn as_service(&self) -> Service {
        match *self {
            Target::Creator { service, .. } => service,
            Target::Discord { .. } => Service::Discord,
        }
    }

    pub async fn try_parse_file() -> Result<Vec<Target>> {
        let mut targets = Vec::new();

        if let Some(files) = &ARGUMENTS.input_files {
            for path in files {
                for line in BufReader::new(File::open(path)?).lines() {
                    match Target::try_from_url(&line?).await {
                        Ok(mut target) => targets.append(&mut target),
                        Err(err) => eprintln!("{err}"),
                    }
                }
            }
        }

        Ok(targets)
    }

    pub async fn parse_args() -> Vec<Target> {
        if let Some(urls) = ARGUMENTS.urls.as_ref() {
            let mut targets = Vec::with_capacity(urls.len());

            for url in urls {
                match Target::try_from_url(url).await {
                    Ok(mut target) => targets.append(&mut target),
                    Err(err) => eprintln!("{err}"),
                }
            }

            targets
        } else {
            Vec::new()
        }
    }

    #[allow(clippy::too_many_lines)]
    async fn try_from_url(url: &str) -> Result<Vec<Self>> {
        let url = url.strip_suffix('/').unwrap_or(url);

        let capture = |re: &Regex| re.captures(url).expect("get captures");
        let extract = |caps: &Captures, name: &str| caps.name(name).map(|m| m.as_str().to_string());
        let extract_unwrap = |caps: &Captures, name: &str| {
            extract(caps, name).expect("extract values from captures")
        };
        let service_user = |caps: &Captures| {
            if let Ok(service) = extract_unwrap(caps, "service").parse::<Service>() {
                Ok((service, extract_unwrap(caps, "user")))
            } else {
                Err(format_err!("Found invalid Service name when parsing URL"))
            }
        };
        let server_channel = |caps: &Captures| (
            extract_unwrap(caps, "server"),
            extract(caps, "channel"),
        );
        let make_paths = |service, user_or_server: &str| {
            (make_pathbuf(service, user_or_server), make_archive_pathbuf(service, user_or_server))
        };

        if RE_LINKED.is_match(url) {
            let caps = capture(&RE_LINKED);

            let (service, user) = service_user(&caps)?;
            let linked = try_fetch_linked_accounts(service, &user).await?;

            let mut targets = Vec::new();

            for info in linked {
                let target = if info.service == "discord" {
                    let (path, archive_path) = make_paths(Service::Discord, &info.id);

                    Target::Discord {
                        server: info.id,
                        channel: None,
                        offset: None,
                        path,
                        archive_path,
                    }
                } else {
                    let (service, user) = (info.service.parse()?, info.id);
                    let (path, archive_path) = make_paths(service, &user);

                    Target::Creator {
                        service,
                        user,
                        subtype: SubType::None,
                        path,
                        archive_path,
                    }
                };

                targets.push(target);
            }

            return Ok(targets);
        }

        let target = if RE_CREATOR.is_match(url) {
            let caps = capture(&RE_CREATOR);

            let (service, user) = service_user(&caps)?;
            let (path, archive_path) = make_paths(service, &user);

            Target::Creator {
                service,
                user,
                subtype: SubType::None,
                path,
                archive_path,
            }
        } else if RE_PAGE.is_match(url) {
            let caps = capture(&RE_PAGE);

            let (service, user) = service_user(&caps)?;
            let (path, archive_path) = make_paths(service, &user);

            Target::Creator {
                service,
                user,
                subtype: SubType::PageOffset(extract_unwrap(&caps, "offset").parse()?),
                path,
                archive_path,
            }
        } else if RE_POST.is_match(url) {
            let caps = capture(&RE_POST);

            let (service, user) = service_user(&caps)?;
            let (path, archive_path) = make_paths(service, &user);

            Target::Creator {
                service,
                user,
                subtype: SubType::Post(extract_unwrap(&caps, "post")),
                path,
                archive_path,
            }
        } else if RE_DISCORD.is_match(url) {
            let caps = capture(&RE_DISCORD);

            let (server, channel) = server_channel(&caps);
            let (path, archive_path) = make_paths(Service::Discord, &server);

            Target::Discord {
                server,
                channel,
                offset: None,
                path,
                archive_path,
            }
        } else if RE_DISCORD_PAGE.is_match(url) {
            let caps = capture(&RE_DISCORD_PAGE);

            let (server, channel) = server_channel(&caps);
            let (path, archive_path) = make_paths(Service::Discord, &server);

            Target::Discord {
                server,
                channel,
                offset: {
                    let offset = extract_unwrap(&caps, "offset").parse()?;
                    if offset % 150 == 0 {
                        Some(offset)
                    } else {
                        return Err(format_err!("Invalid URL: {url}"));
                    }
                },
                path,
                archive_path,
            }
        } else {
            return Err(format_err!("Invalid URL: {url}"));
        };

        Ok(Vec::from_iter([target]))
    }

    pub fn try_read_archive(&self) -> Result<HashSet<String>> {
        let mut archive = File::options()
            .read(true)
            .append(true)
            .create(true)
            .truncate(false)
            .open(self.as_archive_pathbuf())
            .with_context(|| {
                let file = self.to_string();
                let mut buf = String::with_capacity(32 + file.len());
                let _ = write!(buf, "Failed to open archive file for {file}");
                buf
            })?;

        let mut arc_buf = String::new();

        archive.read_to_string(&mut arc_buf)?;

        Ok(arc_buf.lines().map(ToString::to_string).collect())
    }

    pub fn as_pathbuf(&self) -> &PathBuf {
        match self {
            Target::Creator { path, .. } | Target::Discord { path, .. } => path,
        }
    }

    pub fn as_archive_pathbuf(&self) -> &PathBuf {
        match self {
            Target::Creator { archive_path, .. } | Target::Discord { archive_path, .. } => {
                archive_path
            }
        }
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Hash, EnumString, Display, Deserialize, Debug)]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum Service {
    Afdian,
    Boosty,
    CandFans,
    Discord,
    DlSite,
    Fanbox,
    Fansly,
    Fantia,
    Gumroad,
    OnlyFans,
    Patreon,
    SubscribeStar,
}

impl Service {
    pub fn as_static_str(self) -> &'static str {
        #[allow(clippy::enum_glob_use)]
        use Service::*;
        match self {
            Afdian => "afdian",
            Boosty => "boosty",
            CandFans => "candfans",
            Discord => "discord",
            DlSite => "dlsite",
            Fanbox => "fanbox",
            Fansly => "fansly",
            Fantia => "fantia",
            Gumroad => "gumroad",
            OnlyFans => "onlyfans",
            Patreon => "patreon",
            SubscribeStar => "subscribestar",
        }
    }

    pub fn host(self) -> &'static str {
        #[allow(clippy::enum_glob_use)]
        use Service::*;
        match self {
            CandFans | Fansly | OnlyFans => "coomer.st",
            _ => "kemono.cr",
        }
    }
}

fn make_pathbuf(service: Service, user: &str) -> PathBuf {
    PathBuf::from_iter([&ARGUMENTS.output_path, service.as_static_str(), user])
}

fn make_archive_pathbuf(service: Service, user: &str) -> PathBuf {
    PathBuf::from_iter([
        &ARGUMENTS.output_path,
        "db",
        &({
            let service = service.as_static_str();

            let mut file_name = String::with_capacity(service.len() + 1 + user.len() + 4);
            let _ = write!(file_name, "{service}+{user}.txt");

            file_name
        }),
    ])
}
