use std::borrow::BorrowMut;
use std::collections::HashMap;
use std::error::Error;
use std::fmt;

use chrono::{DateTime, Utc};
use log::{debug, error, info, warn};
use reqwest::header::HeaderMap;
use reqwest::{Client, ClientBuilder, RequestBuilder, Response};
use serde::{Deserialize, Serialize};
use url::Url;

use types::asset_info::{AssetInfo, GameToken, OwnershipToken};
use types::asset_manifest::AssetManifest;
use types::download_manifest::DownloadManifest;
use types::entitlement::Entitlement;
use types::library::Library;

use crate::api::types::epic_asset::EpicAsset;
use std::str::FromStr;

/// Module holding the API types
pub mod types;

/// Various API Utils
pub mod utils;

/// Structure that holds all user data
///
/// Needed for login
#[allow(missing_docs)]
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UserData {
    access_token: Option<String>,
    pub expires_in: Option<i64>,
    pub expires_at: Option<DateTime<Utc>>,
    pub token_type: Option<String>,
    refresh_token: Option<String>,
    pub refresh_expires: Option<i64>,
    pub refresh_expires_at: Option<DateTime<Utc>>,
    pub account_id: Option<String>,
    pub client_id: Option<String>,
    pub internal_client: Option<bool>,
    pub client_service: Option<String>,
    #[serde(rename = "displayName")]
    pub display_name: Option<String>,
    pub app: Option<String>,
    pub in_app_id: Option<String>,
    pub device_id: Option<String>,
    #[serde(rename = "errorMessage")]
    pub error_message: Option<String>,
    #[serde(rename = "errorCode")]
    pub error_code: Option<String>,
}

impl UserData {
    /// Creates new UserData Structure
    pub fn new() -> Self {
        UserData {
            access_token: None,
            expires_in: None,
            expires_at: None,
            token_type: None,
            refresh_token: None,
            refresh_expires: None,
            refresh_expires_at: None,
            account_id: None,
            client_id: None,
            internal_client: None,
            client_service: None,
            display_name: None,
            app: None,
            in_app_id: None,
            device_id: None,
            error_message: None,
            error_code: None,
        }
    }

    /// Updates only the present values in the existing user data
    pub fn update(&mut self, new: UserData) {
        if let Some(n) = new.access_token {
            self.access_token = Some(n)
        }
        if let Some(n) = new.expires_in {
            self.expires_in = Some(n)
        }
        if let Some(n) = new.expires_at {
            self.expires_at = Some(n)
        }
        if let Some(n) = new.token_type {
            self.token_type = Some(n)
        }
        if let Some(n) = new.refresh_token {
            self.refresh_token = Some(n)
        }
        if let Some(n) = new.refresh_expires {
            self.refresh_expires = Some(n)
        }
        if let Some(n) = new.refresh_expires_at {
            self.refresh_expires_at = Some(n)
        }
        if let Some(n) = new.account_id {
            self.account_id = Some(n)
        }
        if let Some(n) = new.client_id {
            self.client_id = Some(n)
        }
        if let Some(n) = new.internal_client {
            self.internal_client = Some(n)
        }
        if let Some(n) = new.client_service {
            self.client_service = Some(n)
        }
        if let Some(n) = new.display_name {
            self.display_name = Some(n)
        }
        if let Some(n) = new.app {
            self.app = Some(n)
        }
        if let Some(n) = new.in_app_id {
            self.in_app_id = Some(n)
        }
        if let Some(n) = new.device_id {
            self.device_id = Some(n)
        }
        if let Some(n) = new.error_message {
            self.error_message = Some(n)
        }
        if let Some(n) = new.error_code {
            self.error_code = Some(n)
        }
    }
}

#[derive(Default, Debug, Clone)]
pub(crate) struct EpicAPI {
    client: Client,
    pub(crate) user_data: UserData,
}

/// Error enum for the Epic API
#[derive(Debug)]
pub enum EpicAPIError {
    /// Wrong credentials
    InvalidCredentials,
    /// API error - see the contents
    APIError(String),
    /// Unknown error
    Unknown,
    /// Invalid parameters
    InvalidParams,
    /// Server error
    Server,
}

impl fmt::Display for EpicAPIError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match &*self {
            EpicAPIError::InvalidCredentials => {
                write!(f, "Invalid Credentials")
            }
            EpicAPIError::Unknown => {
                write!(f, "Unknown Error")
            }
            EpicAPIError::Server => {
                write!(f, "Server Error")
            }
            EpicAPIError::APIError(e) => {
                write!(f, "API Error: {}", e)
            }
            EpicAPIError::InvalidParams => {
                write!(f, "Invalid Input Parameters")
            }
        }
    }
}

impl Error for EpicAPIError {
    fn description(&self) -> &str {
        match *self {
            EpicAPIError::InvalidCredentials => "Invalid Credentials",
            EpicAPIError::Unknown => "Unknown Error",
            EpicAPIError::Server => "Server Error",
            EpicAPIError::APIError(_) => "API Error",
            EpicAPIError::InvalidParams => "Invalid Input Parameters",
        }
    }
}

impl EpicAPI {
    pub fn new() -> Self {
        let client = EpicAPI::build_client().build().unwrap();
        EpicAPI {
            client,
            user_data: Default::default(),
        }
    }

    fn build_client() -> ClientBuilder {
        let mut headers = HeaderMap::new();
        headers.insert(
            "User-Agent",
            "UELauncher/12.0.5-15338009+++Portal+Release-Live Windows/6.1.7601.1.0.64bit"
                .parse()
                .unwrap(),
        );
        let client = reqwest::Client::builder()
            .default_headers(headers)
            .cookie_store(true);
        client
    }

    pub async fn start_session(
        &mut self,
        exchange_token: Option<String>,
    ) -> Result<bool, EpicAPIError> {
        let params = match exchange_token {
            None => [
                ("grant_type".to_string(), "refresh_token".to_string()),
                (
                    "refresh_token".to_string(),
                    self.user_data.refresh_token.clone().unwrap(),
                ),
                ("token_type".to_string(), "eg1".to_string()),
            ],
            Some(exchange) => [
                ("grant_type".to_string(), "exchange_code".to_string()),
                ("exchange_code".to_string(), exchange),
                ("token_type".to_string(), "eg1".to_string()),
            ],
        };

        match self
            .client
            .post("https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token")
            .form(&params)
            .basic_auth(
                "34a02cf8f4414e29b15921876da36f9a",
                Some("daafbccc737745039dffe53d94fc76cf"),
            )
            .send()
            .await
        {
            Ok(response) => {
                return self.handle_login_response(response).await;
            }
            Err(e) => {
                error!("{:?}", e);
                Err(EpicAPIError::Unknown)
            }
        }
    }

    async fn handle_login_response(&mut self, response: Response) -> Result<bool, EpicAPIError> {
        if response.status() == reqwest::StatusCode::INTERNAL_SERVER_ERROR {
            error!("Server Error");
            return Err(EpicAPIError::Server);
        }
        let new: UserData = match response.json().await {
            Ok(data) => data,
            Err(e) => {
                error!("{:?}", e);
                return Err(EpicAPIError::Unknown);
            }
        };

        self.user_data.update(new);

        match &self.user_data.error_message {
            None => {}
            Some(m) => {
                error!("{}", m);
                return Err(EpicAPIError::APIError(m.to_string()));
            }
        }
        Ok(true)
    }

    fn authorized_get_client(&self, url: Url) -> RequestBuilder {
        let client = EpicAPI::build_client().build().unwrap();
        self.set_authorization_header(client.clone().get(url))
    }

    fn authorized_post_client(&self, url: Url) -> RequestBuilder {
        let client = EpicAPI::build_client().build().unwrap();
        self.set_authorization_header(client.clone().post(url))
    }

    fn set_authorization_header(&self, rb: RequestBuilder) -> RequestBuilder {
        rb.header(
            "Authorization",
            format!(
                "{} {}",
                self.user_data
                    .token_type
                    .as_ref()
                    .unwrap_or(&"bearer".to_string()),
                self.user_data
                    .access_token
                    .as_ref()
                    .unwrap_or(&"".to_string())
            ),
        )
    }

    pub async fn resume_session(&mut self) -> Result<bool, EpicAPIError> {
        match self.authorized_get_client(Url::parse("https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/verify").unwrap()).send().await {
            Ok(response) => {
                return self.handle_login_response(response).await;
            }
            Err(e) => {
                error!("{:?}", e);
                Err(EpicAPIError::Unknown)
            }
        }
    }

    pub async fn invalidate_sesion(&mut self) -> bool {
        match &self.user_data.access_token {
            None => {}
            Some(access_token) => {
                let url = format!("https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/sessions/kill/{}", access_token);
                let client = EpicAPI::build_client().build().unwrap();
                match client.delete(Url::from_str(&url).unwrap()).send().await {
                    Ok(_) => {
                        info!("Session invalidated");
                        return true;
                    }
                    Err(e) => {
                        warn!("Unable to invalidate session: {}", e)
                    }
                }
            }
        };
        return false;
    }

    pub async fn assets(
        &mut self,
        platform: Option<String>,
        label: Option<String>,
    ) -> Result<Vec<EpicAsset>, EpicAPIError> {
        let plat = platform.unwrap_or("Windows".to_string());
        let lab = label.unwrap_or("Live".to_string());
        let url = format!("https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/{}?label={}", plat, lab);
        match self
            .authorized_get_client(Url::parse(&url).unwrap())
            .send()
            .await
        {
            Ok(response) => {
                if response.status() == reqwest::StatusCode::OK {
                    match response.json().await {
                        Ok(assets) => Ok(assets),
                        Err(e) => {
                            error!("{:?}", e);
                            Err(EpicAPIError::Unknown)
                        }
                    }
                } else {
                    warn!(
                        "{} result: {}",
                        response.status(),
                        response.text().await.unwrap()
                    );
                    Err(EpicAPIError::Unknown)
                }
            }
            Err(e) => {
                error!("{:?}", e);
                Err(EpicAPIError::Unknown)
            }
        }
    }

    pub async fn asset_manifest(
        &self,
        platform: Option<String>,
        label: Option<String>,
        namespace: Option<String>,
        item_id: Option<String>,
        app: Option<String>,
    ) -> Result<AssetManifest, EpicAPIError> {
        if let None = namespace {
            return Err(EpicAPIError::InvalidParams);
        };
        if let None = item_id {
            return Err(EpicAPIError::InvalidParams);
        };
        if let None = app {
            return Err(EpicAPIError::InvalidParams);
        };
        let url = format!("https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/v2/platform/{}/namespace/{}/catalogItem/{}/app/{}/label/{}",
                          platform.clone().unwrap_or("Windows".to_string()), namespace.clone().unwrap(), item_id.clone().unwrap(), app.clone().unwrap(), label.clone().unwrap_or("Live".to_string()));
        match self
            .authorized_get_client(Url::parse(&url).unwrap())
            .send()
            .await
        {
            Ok(response) => {
                if response.status() == reqwest::StatusCode::OK {
                    match response.json::<AssetManifest>().await {
                        Ok(mut manifest) => {
                            manifest.platform = platform;
                            manifest.label = label;
                            manifest.namespace = namespace;
                            manifest.item_id = item_id;
                            manifest.app = app;
                            Ok(manifest)
                        }
                        Err(e) => {
                            error!("{:?}", e);
                            Err(EpicAPIError::Unknown)
                        }
                    }
                } else {
                    warn!(
                        "{} result: {}",
                        response.status(),
                        response.text().await.unwrap()
                    );
                    Err(EpicAPIError::Unknown)
                }
            }
            Err(e) => {
                error!("{:?}", e);
                Err(EpicAPIError::Unknown)
            }
        }
    }

    pub async fn asset_download_manifest(
        &self,
        asset_manifest: AssetManifest,
    ) -> Result<DownloadManifest, EpicAPIError> {
        let base_urls = asset_manifest.url_csv();
        for elem in asset_manifest.elements {
            for manifest in elem.manifests {
                let mut queries: Vec<String> = Vec::new();
                debug!("{:?}", manifest);
                for query in manifest.query_params {
                    queries.push(format!("{}={}", query.name, query.value));
                }
                let url = format!("{}?{}", manifest.uri.to_string(), queries.join("&"));
                let client = EpicAPI::build_client().build().unwrap();
                match client.get(Url::from_str(&url).unwrap()).send().await {
                    Ok(response) => {
                        if response.status() == reqwest::StatusCode::OK {
                            match response.bytes().await {
                                Ok(data) => match DownloadManifest::parse(data.to_vec()) {
                                    None => {
                                        error!("Unable to parse the Download Manifest");
                                        return Err(EpicAPIError::Unknown);
                                    }
                                    Some(mut man) => {
                                        let mut url = manifest.uri.clone();
                                        url.set_path(&match url.path_segments() {
                                            None => "".to_string(),
                                            Some(segments) => {
                                                let mut vec: Vec<&str> = segments.collect();
                                                vec.remove(vec.len() - 1);
                                                vec.join("/")
                                            }
                                        });
                                        url.set_query(None);
                                        url.set_fragment(None);
                                        man.set_custom_field(
                                            "BaseUrl".to_string(),
                                            base_urls.clone(),
                                        );

                                        if let Some(id) = asset_manifest.item_id {
                                            man.set_custom_field(
                                                "CatalogItemId".to_string(),
                                                id.clone(),
                                            );
                                        }
                                        if let Some(label) = asset_manifest.label {
                                            man.set_custom_field(
                                                "BuildLabel".to_string(),
                                                label.clone(),
                                            );
                                        }
                                        if let Some(ns) = asset_manifest.namespace {
                                            man.set_custom_field(
                                                "CatalogNamespace".to_string(),
                                                ns.clone(),
                                            );
                                        }

                                        if let Some(app) = asset_manifest.app {
                                            man.set_custom_field(
                                                "CatalogAssetName".to_string(),
                                                app.clone(),
                                            );
                                        }

                                        println!("{:#?}", man.custom_fields);
                                        man.set_custom_field(
                                            "SourceURL".to_string(),
                                            url.to_string(),
                                        );
                                        return Ok(man);
                                    }
                                },
                                Err(e) => {
                                    error!("{:?}", e);
                                    return Err(EpicAPIError::Unknown);
                                }
                            }
                        } else {
                            warn!(
                                "{} result: {}",
                                response.status(),
                                response.text().await.unwrap()
                            );
                            return Err(EpicAPIError::Unknown);
                        }
                    }
                    Err(e) => {
                        error!("{:?}", e);
                        return Err(EpicAPIError::Unknown);
                    }
                }
            }
        }
        Err(EpicAPIError::Unknown)
    }

    pub async fn asset_info(
        &self,
        asset: EpicAsset,
    ) -> Result<HashMap<String, AssetInfo>, EpicAPIError> {
        let url = format!("https://catalog-public-service-prod06.ol.epicgames.com/catalog/api/shared/namespace/{}/bulk/items?id={}&includeDLCDetails=true&includeMainGameDetails=true&country=us&locale=lc",
                          asset.namespace, asset.catalog_item_id);
        match self
            .authorized_get_client(Url::parse(&url).unwrap())
            .send()
            .await
        {
            Ok(response) => {
                if response.status() == reqwest::StatusCode::OK {
                    match response.json().await {
                        Ok(info) => Ok(info),
                        Err(e) => {
                            error!("{:?}", e);
                            Err(EpicAPIError::Unknown)
                        }
                    }
                } else {
                    warn!(
                        "{} result: {}",
                        response.status(),
                        response.text().await.unwrap()
                    );
                    Err(EpicAPIError::Unknown)
                }
            }
            Err(e) => {
                error!("{:?}", e);
                Err(EpicAPIError::Unknown)
            }
        }
    }

    pub async fn game_token(&self) -> Result<GameToken, EpicAPIError> {
        let url = format!(
            "https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/exchange"
        );
        match self
            .authorized_get_client(Url::parse(&url).unwrap())
            .send()
            .await
        {
            Ok(response) => {
                if response.status() == reqwest::StatusCode::OK {
                    match response.json().await {
                        Ok(token) => Ok(token),
                        Err(e) => {
                            error!("{:?}", e);
                            Err(EpicAPIError::Unknown)
                        }
                    }
                } else {
                    warn!(
                        "{} result: {}",
                        response.status(),
                        response.text().await.unwrap()
                    );
                    Err(EpicAPIError::Unknown)
                }
            }
            Err(e) => {
                error!("{:?}", e);
                Err(EpicAPIError::Unknown)
            }
        }
    }

    pub async fn ownership_token(&self, asset: EpicAsset) -> Result<OwnershipToken, EpicAPIError> {
        let url = match &self.user_data.account_id {
            None => {
                return Err(EpicAPIError::InvalidCredentials);
            }
            Some(id) => {
                format!("https://ecommerceintegration-public-service-ecomprod02.ol.epicgames.com/ecommerceintegration/api/public/platforms/EPIC/identities/{}/ownershipToken",
                        id)
            }
        };
        match self
            .authorized_post_client(Url::parse(&url).unwrap())
            .form(&[(
                "nsCatalogItemId".to_string(),
                format!("{}:{}", asset.namespace, asset.catalog_item_id),
            )])
            .send()
            .await
        {
            Ok(response) => {
                if response.status() == reqwest::StatusCode::OK {
                    match response.json().await {
                        Ok(token) => Ok(token),
                        Err(e) => {
                            error!("{:?}", e);
                            Err(EpicAPIError::Unknown)
                        }
                    }
                } else {
                    warn!(
                        "{} result: {}",
                        response.status(),
                        response.text().await.unwrap()
                    );
                    Err(EpicAPIError::Unknown)
                }
            }
            Err(e) => {
                error!("{:?}", e);
                Err(EpicAPIError::Unknown)
            }
        }
    }

    pub async fn user_entitlements(&self) -> Result<Vec<Entitlement>, EpicAPIError> {
        let url = match &self.user_data.account_id {
            None => {
                return Err(EpicAPIError::InvalidCredentials);
            }
            Some(id) => {
                format!("https://entitlement-public-service-prod08.ol.epicgames.com/entitlement/api/account/{}/entitlements?start=0&count=5000",
                        id)
            }
        };
        match self
            .authorized_get_client(Url::parse(&url).unwrap())
            .send()
            .await
        {
            Ok(response) => {
                if response.status() == reqwest::StatusCode::OK {
                    match response.json().await {
                        Ok(ent) => Ok(ent),
                        Err(e) => {
                            error!("{:?}", e);
                            Err(EpicAPIError::Unknown)
                        }
                    }
                } else {
                    warn!(
                        "{} result: {}",
                        response.status(),
                        response.text().await.unwrap()
                    );
                    Err(EpicAPIError::Unknown)
                }
            }
            Err(e) => {
                error!("{:?}", e);
                Err(EpicAPIError::Unknown)
            }
        }
    }

    pub async fn library_items(&mut self, include_metadata: bool) -> Result<Library, EpicAPIError> {
        let mut library = Library {
            records: vec![],
            response_metadata: Default::default(),
        };
        let mut cursor: Option<String> = None;
        loop {
            let url = match &cursor {
                None => {
                    format!("https://library-service.live.use1a.on.epicgames.com/library/api/public/items?includeMetadata={}", include_metadata)
                }
                Some(c) => {
                    format!("https://library-service.live.use1a.on.epicgames.com/library/api/public/items?includeMetadata={}&cursor={}", include_metadata, c)
                }
            };

            match self
                .authorized_get_client(Url::parse(&url).unwrap())
                .send()
                .await
            {
                Ok(response) => {
                    if response.status() == reqwest::StatusCode::OK {
                        match response.json::<Library>().await {
                            Ok(mut records) => {
                                library.records.append(records.records.borrow_mut());
                                match records.response_metadata {
                                    None => {
                                        break;
                                    }
                                    Some(meta) => match meta.next_cursor {
                                        None => {
                                            break;
                                        }
                                        Some(curs) => {
                                            cursor = Some(curs);
                                        }
                                    },
                                }
                            }
                            Err(e) => {
                                error!("{:?}", e);
                            }
                        }
                    } else {
                        warn!(
                            "{} result: {}",
                            response.status(),
                            response.text().await.unwrap()
                        );
                    }
                }
                Err(e) => {
                    error!("{:?}", e);
                }
            };
            if cursor.is_none() {
                break;
            }
        }
        Ok(library)
    }
}
