// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
//     Copyright (C) 2023 c0repwn3r
//
//     This program is free software: you can redistribute it and/or modify
//     it under the terms of the GNU General Public License as published by
//     the Free Software Foundation, either version 3 of the License, or
//     (at your option) any later version.
//
//     This program is distributed in the hope that it will be useful,
//     but WITHOUT ANY WARRANTY; without even the implied warranty of
//     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//     GNU General Public License for more details.
//
//     You should have received a copy of the GNU General Public License
//     along with this program.  If not, see <https://www.gnu.org/licenses/>.

use std::time::{Duration, SystemTime};
use ipnet::Ipv4Net;
use rocket::{get, post, options, State, error};
use rocket::http::{ContentType, Status};
use rocket::serde::json::Json;
use serde::{Serialize, Deserialize};
use sqlx::PgPool;
use trifid_pki::cert::{NebulaCertificate, NebulaCertificateDetails, serialize_ed25519_private};
use trifid_pki::ed25519_dalek::{SigningKey};
use trifid_pki::rand_core::OsRng;
use crate::auth::TOTPAuthenticatedUserInfo;
use crate::config::TFConfig;
use crate::crypto::{encrypt_with_nonce, generate_random_iv, get_cipher_from_config};
use crate::org::{get_associated_orgs, get_org_by_owner_id, get_users_by_assoc_org, user_has_org_assoc};

#[options("/v1/orgs")]
pub fn options() -> &'static str {
    ""
}
#[options("/v1/org/<_id>")]
pub fn orgidoptions(_id: i32) -> &'static str {
    ""
}
#[options("/v1/org")]
pub fn createorgoptions() -> &'static str {""}

#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct OrglistStruct {
    pub org_ids: Vec<i32>
}


#[get("/v1/orgs")]
pub async fn orglist_req(user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<OrglistStruct>), (Status, String)> {
    // this endpoint lists the associated organizations this user has access to
    let associated_orgs = match get_associated_orgs(user.user_id, db.inner()).await {
        Ok(r) => r,
        Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_DB_QUERY_FAILED", "an error occurred while running the database query", e)))
    };

    Ok((ContentType::JSON, Json(OrglistStruct {
        org_ids: associated_orgs
    })))
}

#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct OrginfoStruct {
    pub org_id: i32,
    pub owner_id: i32,
    pub ca_crt: String,
    pub authorized_users: Vec<i32>
}

#[get("/v1/org/<orgid>")]
pub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> {
    if !user_has_org_assoc(user.user_id, orgid, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))? {
        return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_MISSING_ORG_AUTHORIZATION", "this user does not have permission to access this org")));
    }

    let org = sqlx::query!("SELECT id, owner, ca_crt FROM organizations WHERE id = $1", orgid).fetch_one(db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?;
    let authorized_users = get_users_by_assoc_org(orgid, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?;

    Ok((ContentType::JSON, Json(
        OrginfoStruct {
            org_id: orgid,
            owner_id: org.owner,
            ca_crt: org.ca_crt,
            authorized_users,
        }
    )))
}

#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct CreateCARequest {
    pub ip_ranges: Vec<Ipv4Net>,
    pub subnet_ranges: Vec<Ipv4Net>,
    pub groups: Vec<String>
}

#[post("/v1/org", data = "<req>")]
pub async fn create_org(req: Json<CreateCARequest>, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> {
    if get_org_by_owner_id(user.user_id, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?.is_some() {
        return Err((Status::Conflict, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_USER_OWNS_ORG", "a user can only own one organization at a time")))
    }

    // Generate the CA keypair
    let private_key = SigningKey::generate(&mut OsRng);
    let public_key = private_key.verifying_key();

    // Create the CA certificate
    let mut ca_cert = NebulaCertificate {
        details: NebulaCertificateDetails {
            name: format!("{}'s Organization - Root Signing CA", user.email),
            ips: req.ip_ranges.clone(),
            subnets: req.subnet_ranges.clone(),
            groups: req.groups.clone(),
            not_before: SystemTime::now(),
            not_after: SystemTime::now() + Duration::from_secs(config.ca_certs_valid_for),
            public_key: public_key.to_bytes(),
            is_ca: true,
            issuer: "".to_string(), // This is a self-signed certificate! There is no issuer present
        },
        signature: vec![],
    };
    // Self-sign the CA certificate
    match ca_cert.sign(&private_key) {
        Ok(_) => (),
        Err(e) => {
            error!("[tfapi] security: certificate signature error: {}", e);
            return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_CERT_SIGN_ERROR", "there was an error generating the CA certificate, please try again later")))
        }
    }

    // PEM-encode the CA key
    let ca_key_pem = serialize_ed25519_private(&private_key.to_keypair_bytes());
    // PEM-encode the CA cert
    let ca_cert_pem = match ca_cert.serialize_to_pem() {
        Ok(pem) => pem,
        Err(e) => {
            error!("[tfapi] security: certificate encoding error: {}", e);
            return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_CERT_ENCODE_ERROR", "there was an error encoding the CA certificate, please try again later")))
        }
    };

    // generate an AES iv to use for key encryption
    let iv = generate_random_iv();
    let iv_hex = hex::encode(iv);

    let owner_id = user.user_id;
    let cipher = match get_cipher_from_config(config) {
        Ok(c) => c,
        Err(e) => {
            return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_CREATE_CIPHER", "Unable to build cipher construct, please try again later", e)));
        }
    };
    let ca_key = match encrypt_with_nonce(&ca_key_pem, iv, &cipher) {
        Ok(key) => hex::encode(key),
        Err(e) => {
            return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_ENCRYPT_KEY", "Unable to build cipher construct, please try again later", e)));
        }
    };
    let ca_crt = hex::encode(ca_cert_pem);

    let result = sqlx::query!("INSERT INTO organizations (owner, ca_key, ca_crt, iv) VALUES ($1, $2, $3, $4) RETURNING id", owner_id, ca_key, ca_crt, iv_hex).fetch_one(db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?;

    Ok((ContentType::JSON, Json(
        OrginfoStruct {
            org_id: result.id,
            owner_id,
            ca_crt,
            authorized_users: vec![owner_id],
        }
    )))
}