Documentation
use crate::{endpoints::base::IDP, *};
use consts::{CLIENT_ID, TIMEOUT, USER_AGENT as UA};
use http::{HeaderMap, HeaderValue, Response, header};
use serde::Deserialize;
use std::{collections::BTreeMap, collections::BTreeSet, env, fs::File, io::Write, path::PathBuf};
use ureq::typestate::{AgentScope, WithBody, WithoutBody};
use ureq::{Agent, Body, RequestBuilder};

/// checks for a special "please don't access the network" request: errors out if env var `NO_NET=1`
fn check_no_net() -> Res<()> {
    if env::var("NO_NET").is_ok_and(|nn| nn == "1") {
        log::info!("manually triggered 'no network connection' error");
        return Err("manual NO_NET error".into());
    }
    Ok(())
}

/// log `req`uestBuilder, send it, log Response and return it
fn log_req_n_call(req: RequestBuilder<WithoutBody>) -> Res<Response<Body>> {
    check_no_net()?;
    log::debug!("request to be sent: {req:?}");
    let resp = req.call()?;
    log::debug!("got response: {resp:?}");
    Ok(resp)
}

/// log `req`uest, send it with `form`, log Response and return it
fn log_req_n_send_form<I, K, V>(req: RequestBuilder<WithBody>, form: I) -> Res<Response<Body>>
where
    I: IntoIterator<Item = (K, V)>,
    K: AsRef<str>,
    V: AsRef<str>,
{
    check_no_net()?;
    log::debug!("request to be sent: {req:?}");
    let resp = req.send_form(form)?;
    log::debug!("got response: {resp:?}");
    Ok(resp)
}

/// get a configurable [`ureq::Agent`] with a [`TIMEOUT`]
fn agent_config() -> ureq::config::ConfigBuilder<AgentScope> {
    Agent::config_builder().timeout_global(Some(TIMEOUT))
}

#[derive(Debug, Clone)]
pub struct Account {
    /// the id of the school the user goes to, usually looks like:  "klik" + 9 numbers: `klikXXXXXXXXX`
    pub schoolid: String,
    /// text that shall be replaced whenever found in a response, no fuzzy matching or regex supported ATM
    /// eg.: a subject's name: `["Mathematics".to_owned(), "maths".to_owned()]`
    pub rename: BTreeMap<String, String>,
    /// the request agent
    agent: Agent,
}
impl Account {
    pub fn new(schoolid: String, rename: BTreeMap<String, String>) -> Self {
        Self {
            schoolid,
            rename,
            agent: agent_config().build().new_agent(),
        }
    }
}
// fetch_.*
impl Account {
    pub fn fetch_info(&self, headers: &HeaderMap) -> Res<UserInfo> {
        self.fetch_single::<UserInfo, UserInfo>((), headers)
    }
    pub fn fetch_evals(&self, interval: OptIrval, headers: &HeaderMap) -> Res<Vec<Evaluation>> {
        self.fetch_vec(interval, headers)
    }
    pub fn fetch_timetable(
        &self,
        interval: (chrono::NaiveDate, chrono::NaiveDate),
        headers: &HeaderMap,
    ) -> Res<Vec<Lesson>> {
        self.fetch_vec(interval, headers)
    }
    pub fn fetch_absences(&self, interval: OptIrval, headers: &HeaderMap) -> Res<Vec<Absence>> {
        self.fetch_vec(interval, headers)
    }
    pub fn fetch_classes(&self, headers: &HeaderMap) -> Res<Vec<Class>> {
        self.fetch_vec((), headers)
    }
    pub fn fetch_announced_tests(
        &self,
        interval: OptIrval,
        headers: &HeaderMap,
    ) -> Res<Vec<AnnouncedTest>> {
        self.fetch_vec(interval, headers)
    }
}

// messages
impl Account {
    pub fn fetch_full_msg(
        &self,
        msg_oview: Option<&MsgOview>,
        headers: &HeaderMap,
    ) -> Res<MsgItem> {
        let id = msg_oview.map(|mov| mov.azonosito);
        self.fetch_single::<MsgItem, MsgItem>(id, headers)
    }
    pub fn fetch_note_msgs(&self, interval: OptIrval, headers: &HeaderMap) -> Res<Vec<NoteMsg>> {
        self.fetch_vec(interval, headers)
    }
    pub fn fetch_msg_oview_of_kind(
        &self,
        msg_kind: MsgKind,
        headers: &HeaderMap,
    ) -> Res<Vec<MsgOview>> {
        self.fetch_vec(msg_kind, headers)
    }
    pub fn fetch_msg_oviews(&self, headers: &HeaderMap) -> Res<Vec<MsgOview>> {
        Ok([
            self.fetch_msg_oview_of_kind(MsgKind::Recv, headers)?,
            self.fetch_msg_oview_of_kind(MsgKind::Sent, headers)?,
            self.fetch_msg_oview_of_kind(MsgKind::Del, headers)?,
        ]
        .concat())
    }
    pub fn download_attachment_to(
        &self,
        id: u32,
        out_path: PathBuf,
        headers: &HeaderMap,
    ) -> Res<()> {
        let mut f = File::create(out_path)?;
        let resp = self.get_response::<Attachment>(id, headers)?;
        let content = resp.into_body().read_to_vec()?;
        f.write_all(&content)?;
        Ok(())
    }
}

fn store_cookies(cookie_jar: &mut BTreeSet<HeaderValue>, resp: &Response<Body>) {
    let got_cookies = resp.headers().get_all(header::SET_COOKIE);
    log::debug!("cookies received: {got_cookies:?}");
    for got_cookie in got_cookies {
        let raw_set_cookie = got_cookie.to_str().unwrap();
        let cookie_val = raw_set_cookie.split(';').next().unwrap();
        cookie_jar.insert(cookie_val.parse().unwrap());
    }
    log::debug!("cookie jar after storing: {cookie_jar:?}");
}
fn add_cookies<B>(cookies: &BTreeSet<HeaderValue>, req: &mut RequestBuilder<B>) {
    log::debug!("cookies to be added to request: {cookies:?}");
    if let Some(headers) = req.headers_mut() {
        for cookie in cookies {
            headers.append(header::COOKIE, cookie.clone());
        }
    }
    log::debug!("request's headers w/ cookies: {:?}", req.headers_ref());
}

// token
impl Account {
    pub fn refresh_token_resp(schoolid: &str, refresh_token: &str) -> Res<Response<Body>> {
        let form = [
            ("institute_code", schoolid),
            ("grant_type", "refresh_token"),
            ("client_id", consts::CLIENT_ID),
            ("refresh_token", refresh_token),
        ];
        let req = agent_config()
            .build()
            .new_agent()
            .post(Token::ep_url())
            .header(header::USER_AGENT, UA);

        log_req_n_send_form(req, form)
    }
    pub fn refresh_token(&self, refresh_token: &str) -> Res<Token> {
        let text = Self::refresh_token_resp(&self.schoolid, refresh_token)?
            .into_body()
            .read_to_string()?;
        log::debug!("token as text/json: {text}");
        let token = serde_json::from_str(&text)?;
        log::debug!("token: {token:?}");
        Ok(token)
    }
    pub fn get_token_resp(&self, password: String, userid: String) -> Res<Response<Body>> {
        let client = agent_config()
            .max_redirects(0) // this is needed so the client doesnt follow redirects by itself like a dumb little sheep
            .build()
            .new_agent();

        // initial login page
        let initial_url = format!(
            "{}/Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fprompt%3Dlogin%26nonce%3DwylCrqT4oN6PPgQn2yQB0euKei9nJeZ6_ffJ-VpSKZU%26response_type%3Dcode%26code_challenge_method%3DS256%26scope%3Dopenid%2520email%2520offline_access%2520kreta-ellenorzo-webapi.public%2520kreta-eugyintezes-webapi.public%2520kreta-fileservice-webapi.public%2520kreta-mobile-global-webapi.public%2520kreta-dkt-webapi.public%2520kreta-ier-webapi.public%26code_challenge%3DHByZRRnPGb-Ko_wTI7ibIba1HQ6lor0ws4bcgReuYSQ%26redirect_uri%3Dhttps%253A%252F%252Fmobil.e-kreta.hu%252Fellenorzo-student%252Fprod%252Foauthredirect%26client_id%3D{CLIENT_ID}%26state%3Dkreten_student_mobile%26suppressed_prompt%3Dlogin",
            Token::base_url("")
        );
        let req = client.get(initial_url);
        let resp = log_req_n_call(req)?;
        let mut cookie_jar = BTreeSet::new();
        store_cookies(&mut cookie_jar, &resp);
        let raw_login_page_html = resp.into_body().read_to_string()?;

        const NORVT: &str = "RVT code not found in login-page html";
        log::debug!("login_page, html: {raw_login_page_html}");
        // Parse RVT token from HTML from sg like below:
        // `<input name="__RequestVerificationToken" type="hidden" value="$magic-bean" /></form>`
        let rvt_form_line = raw_login_page_html
            .lines()
            .find(|l| l.contains("__RequestVerificationToken"))
            .ok_or(NORVT)?;
        log::debug!("line containing html RVT form: {rvt_form_line:?}");
        let rvt_start = rvt_form_line.find("value=").ok_or(NORVT)? + 7; // idx of actual code start
        log::debug!("rvt start idx: {rvt_start}");
        let rvt_len = rvt_form_line[rvt_start..].find('"').ok_or(NORVT)?;
        log::debug!("rvt length: {rvt_len}");
        let rvt = &rvt_form_line[rvt_start..rvt_start + rvt_len];
        log::debug!("rvt: {rvt:?}");

        let auth_query = (userid, password, self.schoolid.clone(), rvt.to_string());
        let form_data = Token::query(auth_query);

        let login_url = format!("{}/account/login", Token::base_url(""));
        // let headers = Token::headers(&"")?.unwrap();
        // perform login with credentials
        let mut req = client.post(login_url).header("User-Agent", UA);
        add_cookies(&cookie_jar, &mut req);
        let resp = log_req_n_send_form(req, form_data)?;
        store_cookies(&mut cookie_jar, &resp);

        // Check if the response status is 200 (OK)
        if !resp.status().is_success() {
            let stat = resp.status();
            return Err(format!("login failed: check your credentials; status: {stat}").into());
        }

        let mut req = client.get(format!("{IDP}/connect/authorize/callback?prompt=login&nonce=wylCrqT4oN6PPgQn2yQB0euKei9nJeZ6_ffJ-VpSKZU&response_type=code&code_challenge_method=S256&scope=openid%20email%20offline_access%20kreta-ellenorzo-webapi.public%20kreta-eugyintezes-webapi.public%20kreta-fileservice-webapi.public%20kreta-mobile-global-webapi.public%20kreta-dkt-webapi.public%20kreta-ier-webapi.public&code_challenge=HByZRRnPGb-Ko_wTI7ibIba1HQ6lor0ws4bcgReuYSQ&redirect_uri=https%3A%2F%2Fmobil.e-kreta.hu%2Fellenorzo-student%2Fprod%2Foauthredirect&client_id={CLIENT_ID}&state=kreten_student_mobile&suppressed_prompt=login"));
        add_cookies(&cookie_jar, &mut req);
        let resp = log_req_n_call(req)?;
        store_cookies(&mut cookie_jar, &resp);

        // Follow the redirect manually to get the code
        let location = resp
            .headers()
            .get(header::LOCATION)
            .ok_or("no Location header after login redirect")?
            .to_str()?;

        const NOCODE: &str = "auth code not found";
        // Extract code from the location header
        let uri: http::Uri = location.parse()?;
        log::debug!("uri: {uri}");
        let query = uri.query().ok_or(NOCODE)?;
        log::debug!("query: {query}");

        let code_start_ix = query.find("code").ok_or(NOCODE)? + 5;
        log::debug!("code start index: {code_start_ix}");
        let code_len = query[code_start_ix..].find('&').ok_or(NOCODE)?;
        log::debug!("code len: {code_len}");
        let code = &query[code_start_ix..code_start_ix + code_len];
        log::debug!("code: {code:?}");

        // magic beans bought on ebay
        const REDIR_URI: &str = "https://mobil.e-kreta.hu/ellenorzo-student/prod/oauthredirect";
        const CODE_VERIF: &str = "DSpuqj_HhDX4wzQIbtn8lr8NLE5wEi1iVLMtMK0jY6c";
        // Exchange code for access token
        let token_data = [
            ("code", code),
            ("code_verifier", CODE_VERIF),
            ("redirect_uri", REDIR_URI),
            ("client_id", CLIENT_ID),
            ("grant_type", "authorization_code"),
        ];

        let mut req = client.post(Token::ep_url());
        add_cookies(&cookie_jar, &mut req);
        log_req_n_send_form(req, token_data)
    }
    pub fn fetch_token(&self, password: String, userid: String) -> Res<Token> {
        let text = self
            .get_token_resp(password, userid)?
            .into_body()
            .read_to_string()?;
        log::debug!("token as text/json: {text}");
        let token = serde_json::from_str(&text)?;
        log::debug!("token: {token:?}");
        Ok(token)
    }
}
impl Account {
    // /// get headers which are necessary for making certain requests
    // pub fn headers(&self) -> Res<HeaderMap> {
    //     Ok(HeaderMap::from_iter([
    //         (
    //             header::AUTHORIZATION,
    // INFO: token should be cached
    //             format!("Bearer {}", self.fetch_token()?.access_token).parse()?,
    //         ),
    //         (header::USER_AGENT, UA.parse()?),
    //     ]))
    // }

    pub fn get_response<E>(&self, query: E::Args, headers: &HeaderMap) -> Res<Response<Body>>
    where
        E: Endpoint + for<'a> Deserialize<'a>,
    {
        let base = E::base_url(&self.schoolid);
        let uri = [base, E::path(&query)].concat();
        log::debug!("uri: {uri}");
        let query = E::query(query);
        log::debug!("query: {query:?}");
        let mut req = self.agent.get(uri).query_pairs(query);
        if let Some(req_headers) = req.headers_mut() {
            *req_headers = headers.clone();
            log::debug!("headers: {req_headers:?}");
        }
        log_req_n_call(req)
    }
    pub fn fetch_single<E, D>(&self, query: E::Args, headers: &HeaderMap) -> Res<D>
    where
        E: Endpoint + for<'a> Deserialize<'a>,
        D: for<'a> Deserialize<'a> + std::fmt::Debug,
    {
        let resp = self.get_response::<E>(query, headers)?;
        let mut txt = resp.into_body().read_to_string()?;
        if !env::var("NO_RENAME").is_ok_and(|nn| nn == "1") {
            for (from, to) in &self.rename {
                txt = txt.replace(from, to);
            } // might be slow, but who cares
        }
        log::debug!("fetched text: {txt:?}");
        let deserd = serde_json::from_str(&txt)?;
        log::debug!("deserialized fetched text to: {deserd:?}");
        Ok(deserd)
    }

    pub fn fetch_vec<E>(&self, query: E::Args, headers: &HeaderMap) -> Res<Vec<E>>
    where
        E: Endpoint + for<'a> Deserialize<'a>,
    {
        let fetch_single = self.fetch_single::<E, Vec<E>>(query, headers);
        log::debug!("fetched vec: {fetch_single:?}");
        fetch_single
    }
}