use anyhow::Result;
use colored::Colorize;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use std::path::Path;
use tracing::debug;
use ureq::{RequestBuilder, get, http::HeaderValue, typestate::WithoutBody};

use crate::{
    cable::{CABLE_DIR_NAME, CHANNEL_FILE_FORMAT},
    channels::prototypes::{BinaryRequirement, ChannelPrototype},
    config::get_config_dir,
};

#[derive(Debug, Clone, serde::Deserialize)]
struct GhNode {
    name: String,
    #[serde(rename = "type")]
    kind: NodeType,
    download_url: Option<String>,
}

impl GhNode {
    pub fn is_file(&self) -> bool {
        matches!(self.kind, NodeType::File)
    }
}

#[derive(Debug, Clone, serde::Deserialize)]
enum NodeType {
    #[serde(rename = "file")]
    File,
    #[serde(rename = "dir")]
    Directory,
}

const GITHUB_API_BASE_URL: &str =
    "https://api.github.com/repos/alexpasmantier/television/contents/";

fn make_gh_request(url: &str) -> Result<RequestBuilder<WithoutBody>> {
    let mut request = get(url).header("User-Agent", "television-client");

    let headers = request.headers_mut().unwrap();
    if let Ok(token) = std::env::var("GITHUB_TOKEN") {
        debug!("Using GITHUB_TOKEN environment variable");
        headers.insert("Authorization", HeaderValue::try_from(token)?);
    }

    Ok(request)
}

fn make_gh_content_request(
    gh_dir: &Path,
    git_ref: &str,
) -> Result<Vec<GhNode>> {
    let url = format!("{}{}", GITHUB_API_BASE_URL, gh_dir.to_str().unwrap());
    debug!("Making GitHub API request to: {}", url);
    make_gh_request(&url)?
        .header("Accept", "application/vnd.github+json")
        .query("ref", git_ref)
        .call()
        .map_err(|e| {
            anyhow::anyhow!("Request to '{}' failed with: {}", url, e)
        })
        .map(|response| {
            if response.status().is_success() {
                serde_json::from_str::<Vec<GhNode>>(
                    &response.into_body().read_to_string()?,
                )
                .map_err(|e| anyhow::anyhow!("Failed to parse JSON: {}", e))
            } else {
                Err(anyhow::anyhow!("Failed to fetch data from GitHub API"))
            }
        })?
}

fn fetch_raw_content_from_url(url: &str) -> Result<String> {
    let request = make_gh_request(url)?;
    let response = request.call()?;

    if response.status().is_success() {
        Ok(response.into_body().read_to_string()?)
    } else {
        Err(anyhow::anyhow!(
            "Failed to fetch raw content from URL: {}",
            url
        ))
    }
}

struct DownloadedPrototype {
    pub name: String,
    pub content: String,
}

impl DownloadedPrototype {
    pub fn new(name: String, content: String) -> Self {
        Self { name, content }
    }
}

#[cfg(unix)]
const DEFAULT_CABLE_DIR_PATH: &str = "cable/unix";
#[cfg(windows)]
const DEFAULT_CABLE_DIR_PATH: &str = "cable/windows";

fn get_default_prototypes_from_repo(
    tv_version: &str,
) -> Result<Vec<DownloadedPrototype>> {
    let nodes = make_gh_content_request(
        Path::new(DEFAULT_CABLE_DIR_PATH),
        tv_version,
    )?;
    for node in &nodes {
        println!(
            "  Discovered channel: {}\t\tdownload url: {}",
            node.name.blue().bold(),
            node.download_url.as_deref().unwrap_or("N/A").blue().bold()
        );
    }
    Ok(nodes
        .par_iter()
        .filter_map(|node| {
            if node.is_file() {
                node.download_url.clone()
            } else {
                None
            }
        })
        .filter_map(|url| fetch_raw_content_from_url(&url).ok())
        .filter_map(|content| {
            let name = toml::from_str::<ChannelPrototype>(&content)
                .map(|p| p.metadata.name)
                .ok()?;
            Some(DownloadedPrototype::new(name, content))
        })
        .collect())
}

pub fn update_local_channels(force: &bool) -> Result<()> {
    let tv_version = env!("CARGO_PKG_VERSION");
    println!(
        "{}{}",
        "Fetching cable channels for ".bold(),
        tv_version.green().bold()
    );
    let default_prototypes = get_default_prototypes_from_repo(tv_version)?;
    println!("{}", "\nSaving channels locally...".bold());
    let cable_path = get_config_dir().join(CABLE_DIR_NAME);
    if !cable_path.exists() {
        println!("  Creating cable directory at {}", cable_path.display());
        std::fs::create_dir_all(&cable_path)?;
    }
    for p in default_prototypes {
        let file_path =
            cable_path.join(&p.name).with_extension(CHANNEL_FILE_FORMAT);
        // if the file already exists, don't overwrite it
        if file_path.exists() && !force {
            println!(
                "  Channel {} already exists at {}, SKIPPING...",
                p.name.blue().bold(),
                file_path.display().to_string().yellow().bold()
            );
            continue;
        }
        // skip prototypes with unmet binary requirements unless force is true
        if !force {
            let mut channel_requirements =
                toml::from_str::<ChannelPrototype>(&p.content)?
                    .metadata
                    .requirements;

            channel_requirements
                .iter_mut()
                .for_each(BinaryRequirement::init);

            let missing_requirements = channel_requirements
                .iter_mut()
                .filter(|r| !r.is_met())
                .collect::<Vec<&mut BinaryRequirement>>();

            if !missing_requirements.is_empty() {
                println!(
                    "  Channel {} requirement {} is unavailable on this system, SKIPPING...",
                    p.name.blue().bold(),
                    missing_requirements
                        .iter()
                        .map(|r| r.bin_name.as_str())
                        .collect::<Vec<&str>>()
                        .join(", ")
                        .red()
                        .bold()
                );
                continue;
            }
        }

        std::fs::write(&file_path, p.content)?;
        println!(
            "  Saved channel {} to {}",
            p.name.blue().bold(),
            file_path.display().to_string().yellow().bold()
        );
    }
    println!(
        "{}",
        "\nCable channels updated successfully.".green().bold()
    );
    Ok(())
}
