use oiseau::config::{Configuration, DatabaseConfig};
use pathbufd::PathBufD;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::Result;

/// Security configuration.
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct SecurityConfig {
    /// If registrations are enabled.
    #[serde(default = "default_security_registration_enabled")]
    pub registration_enabled: bool,
    /// The name of the header which will contain the real IP of the connecting user.
    #[serde(default = "default_real_ip_header")]
    pub real_ip_header: String,
}

fn default_security_registration_enabled() -> bool {
    true
}

fn default_real_ip_header() -> String {
    "CF-Connecting-IP".to_string()
}

impl Default for SecurityConfig {
    fn default() -> Self {
        Self {
            registration_enabled: default_security_registration_enabled(),
            real_ip_header: default_real_ip_header(),
        }
    }
}

/// Directories configuration.
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct DirsConfig {
    /// HTML templates directory.
    #[serde(default = "default_dir_templates")]
    pub templates: String,
    /// Static files directory.
    #[serde(default = "default_dir_assets")]
    pub assets: String,
    /// Media (user avatars/banners) files directory.
    #[serde(default = "default_dir_media")]
    pub media: String,
    /// The icons files directory.
    #[serde(default = "default_dir_icons")]
    pub icons: String,
    /// The directory which holds your `rustdoc` (`cargo doc`) output. The directory should
    /// exist, but it isn't required to actually have anything in it.
    #[serde(default = "default_dir_rustdoc")]
    pub rustdoc: String,
}

fn default_dir_templates() -> String {
    "html".to_string()
}

fn default_dir_assets() -> String {
    "public".to_string()
}

fn default_dir_media() -> String {
    "media".to_string()
}

fn default_dir_icons() -> String {
    "icons".to_string()
}

fn default_dir_rustdoc() -> String {
    "reference".to_string()
}

impl Default for DirsConfig {
    fn default() -> Self {
        Self {
            templates: default_dir_templates(),
            assets: default_dir_assets(),
            media: default_dir_media(),
            icons: default_dir_icons(),
            rustdoc: default_dir_rustdoc(),
        }
    }
}

impl Configuration for Config {
    fn db_config(&self) -> DatabaseConfig {
        self.database.to_owned()
    }
}

/// Policies config (TOS/privacy)
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct PoliciesConfig {
    /// The link to your terms of service page.
    /// This is relative to `/auth/register` on the site.
    ///
    /// If your TOS is an HTML file located in `./public`, you can put
    /// `/public/tos.html` here (or something).
    pub terms_of_service: String,
    /// The link to your privacy policy page.
    /// This is relative to `/auth/register` on the site.
    ///
    /// Same deal as terms of service page.
    pub privacy: String,
    /// The link to your refunds policy page.
    pub refunds: String,
    /// The time (in ms since unix epoch) in which the site's policies last updated.
    ///
    /// This is required to automatically ask users to re-consent to policies.
    ///
    /// In user whose consent time in LESS THAN this date will be shown a dialog to re-consent to the policies.
    ///
    /// You can get this easily by running `echo "console.log(new Date().getTime())" | node`.
    #[serde(default)]
    pub last_updated: usize,
}

impl Default for PoliciesConfig {
    fn default() -> Self {
        Self {
            terms_of_service: "/public/tos.html".to_string(),
            privacy: "/public/privacy.html".to_string(),
            refunds: "/public/refunds.html".to_string(),
            last_updated: 0,
        }
    }
}

/// Cloudflare Turnstile configuration
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct TurnstileConfig {
    pub site_key: String,
    pub secret_key: String,
}

impl Default for TurnstileConfig {
    fn default() -> Self {
        Self {
            site_key: "1x00000000000000000000AA".to_string(), // always passing, visible
            secret_key: "1x0000000000000000000000000000000AA".to_string(), // always passing
        }
    }
}

#[derive(Clone, Serialize, Deserialize, Debug, Default)]
pub struct ConnectionsConfig {
    /// <https://www.last.fm/api/authspec>
    /// <https://www.last.fm/api/account/create>
    #[serde(default)]
    pub last_fm_key: Option<String>,
    /// <https://www.last.fm/api/authspec>
    /// <https://www.last.fm/api/account/create>
    #[serde(default)]
    pub last_fm_secret: Option<String>,
}

/// Configuration for Stripe integration.
///
/// User IDs are sent to Stripe through the payment link.
/// <https://docs.stripe.com/payment-links/url-parameters#streamline-reconciliation-with-a-url-parameter>
///
/// # Testing
///
/// - Run `stripe login` using the Stripe CLI
/// - Run `stripe listen --forward-to localhost:4118/api/v2/service_hooks/stripe`
/// - Use testing card numbers: <https://docs.stripe.com/testing?testing-method=card-numbers#visa>
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
pub struct StripeConfig {
    /// Your Stripe API secret.
    pub secret: String,
    /// Payment links from the Stripe dashboard.
    ///
    /// 1. Create a product and set the price for your membership
    /// 2. Set the product price to a recurring subscription
    /// 3. Create a payment link for the new product
    /// 4. The payment link pasted into this config field should NOT include a query string
    pub payment_links: StripePaymentLinks,
    /// To apply benefits to user accounts, you should then go into the Stripe developer
    /// "workbench" and create a new webhook. The webhook needs the scopes:
    /// `invoice.payment_succeeded`, `customer.subscription.deleted`, `checkout.session.completed`, `charge.succeeded`.
    ///
    /// The webhook's destination address should be `{your server origin}/api/v2/service_hooks/stripe`.
    ///
    /// The signing secret can be found on the right after you have created the webhook.
    pub webhook_signing_secret: String,
    /// The URL of your customer billing portal.
    ///
    /// <https://docs.stripe.com/no-code/customer-portal>
    pub billing_portal_url: String,
    /// The text representation of prices. (like `$4 USD`)
    pub price_texts: StripePriceTexts,
    /// Product IDs from the Stripe dashboard.
    ///
    /// These are checked when we receive a webhook to ensure we provide the correct product.
    pub product_ids: StripeProductIds,
}

#[derive(Clone, Serialize, Deserialize, Debug, Default)]
pub struct StripePriceTexts {
    pub supporter: String,
}

#[derive(Clone, Serialize, Deserialize, Debug, Default)]
pub struct StripePaymentLinks {
    pub supporter: String,
}

#[derive(Clone, Serialize, Deserialize, Debug, Default)]
pub struct StripeProductIds {
    pub supporter: String,
}

/// Manuals config (search help, etc)
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct ManualsConfig {
    /// The page shown for help with search syntax.
    pub search_help: String,
}

impl Default for ManualsConfig {
    fn default() -> Self {
        Self {
            search_help: "".to_string(),
        }
    }
}

#[derive(Clone, Serialize, Deserialize, Debug, Default)]
pub struct ServiceHostsConfig {
    /// Buckets host <https://trisua.com/t/buckets>.
    pub buckets: String,
    /// Tawny host <https://trisua.com/t/tawny>.
    #[serde(default)]
    pub tawny: String,
    /// Shrimpcamp Autter host <https://shrimpcamp.com>.
    #[serde(default)]
    pub dashboard: String,
    #[serde(default = "default_overkit")]
    pub overkit: String,
}

fn default_overkit() -> String {
    "http://localhost:8026".to_string()
}

#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub enum StringBan {
    /// An exact string.
    String(String),
    /// A unicode codepoint.
    Unicode(u32),
}

impl Default for StringBan {
    fn default() -> Self {
        Self::String(String::new())
    }
}

/// Configuration file
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Config {
    /// The name of the app.
    #[serde(default = "default_name")]
    pub name: String,
    /// The description of the app.
    #[serde(default = "default_description")]
    pub description: String,
    /// The port to serve the server on.
    #[serde(default = "default_port")]
    pub port: u16,
    /// A list of hosts which cannot be proxied through the image proxy.
    ///
    /// They will return the default banner image instead of proxying.
    ///
    /// It is recommended to put the host of your own public server in this list in
    /// order to prevent a way too easy DOS.
    #[serde(default = "default_banned_hosts")]
    pub banned_hosts: Vec<String>,
    /// The main public host of the server. **Not** used to check against banned hosts,
    /// so this host should be included in there as well.
    #[serde(default = "default_host")]
    pub host: String,
    /// The main public host of the required microservices.
    #[serde(default = "default_service_hosts")]
    pub service_hosts: ServiceHostsConfig,
    /// Database security.
    #[serde(default = "default_security")]
    pub security: SecurityConfig,
    /// The locations where different files should be matched.
    #[serde(default = "default_dirs")]
    pub dirs: DirsConfig,
    /// Database configuration.
    #[serde(default = "default_database")]
    pub database: DatabaseConfig,
    /// A list of usernames which cannot be used. This also includes community names.
    #[serde(default = "default_banned_usernames")]
    pub banned_usernames: Vec<String>,
    /// Configuration for your site's policies (terms of service, privacy).
    #[serde(default = "default_policies")]
    pub policies: PoliciesConfig,
    /// Configuration for Cloudflare Turnstile.
    #[serde(default = "default_turnstile")]
    pub turnstile: TurnstileConfig,
    #[serde(default)]
    pub connections: ConnectionsConfig,
    #[serde(default)]
    pub stripe: Option<StripeConfig>,
    /// The relative paths to manuals.
    #[serde(default)]
    pub manuals: ManualsConfig,
    /// A list of banned content in posts.
    #[serde(default)]
    pub banned_data: Vec<StringBan>,
    /// A banner shown on every page of the site.
    #[serde(default)]
    pub persistent_banner: String,
}

fn default_name() -> String {
    "Tetratto".to_string()
}

fn default_description() -> String {
    "Tetratto (next-gen)".to_string()
}

fn default_port() -> u16 {
    4118
}

fn default_banned_hosts() -> Vec<String> {
    Vec::new()
}

fn default_host() -> String {
    String::new()
}

fn default_service_hosts() -> ServiceHostsConfig {
    ServiceHostsConfig::default()
}

fn default_security() -> SecurityConfig {
    SecurityConfig::default()
}

fn default_dirs() -> DirsConfig {
    DirsConfig::default()
}

fn default_database() -> DatabaseConfig {
    DatabaseConfig::default()
}

fn default_banned_usernames() -> Vec<String> {
    vec![
        "admin".to_string(),
        "owner".to_string(),
        "moderator".to_string(),
        "api".to_string(),
        "communities".to_string(),
        "community".to_string(),
        "notification".to_string(),
        "post".to_string(),
        "anonymous".to_string(),
        "search".to_string(),
        "app".to_string(),
    ]
}

fn default_policies() -> PoliciesConfig {
    PoliciesConfig::default()
}

fn default_turnstile() -> TurnstileConfig {
    TurnstileConfig::default()
}

fn default_connections() -> ConnectionsConfig {
    ConnectionsConfig::default()
}

fn default_manuals() -> ManualsConfig {
    ManualsConfig::default()
}

fn default_banned_data() -> Vec<StringBan> {
    Vec::new()
}

impl Default for Config {
    fn default() -> Self {
        Self {
            name: default_name(),
            description: default_description(),
            port: default_port(),
            banned_hosts: default_banned_hosts(),
            host: default_host(),
            service_hosts: default_service_hosts(),
            database: default_database(),
            security: default_security(),
            dirs: default_dirs(),
            banned_usernames: default_banned_usernames(),
            policies: default_policies(),
            turnstile: default_turnstile(),
            connections: default_connections(),
            stripe: None,
            manuals: default_manuals(),
            banned_data: default_banned_data(),
            persistent_banner: String::new(),
        }
    }
}

impl Config {
    /// Read configuration file into [`Config`]
    pub fn read(contents: String) -> Self {
        toml::from_str::<Self>(&contents).unwrap()
    }

    /// Pull configuration file
    pub fn get_config() -> Self {
        let path = PathBufD::current().join("app.toml");

        match fs::read_to_string(&path) {
            Ok(c) => Config::read(c),
            Err(_) => {
                Self::update_config(Self::default()).expect("failed to write default config");
                Self::default()
            }
        }
    }

    /// Update configuration file
    pub fn update_config(contents: Self) -> Result<()> {
        let c = fs::canonicalize(".").unwrap();
        let here = c.to_str().unwrap();

        fs::write(
            format!("{here}/app.toml"),
            toml::to_string_pretty::<Self>(&contents).unwrap(),
        )
    }
}
