use crate::{
  AppState,
  auth::SessionAuthorization,
  error::{Error, Result},
};
use axum::{
  Json,
  Router,
  extract::{Path, State},
  http::StatusCode,
  routing::post,
};
use serde::{Deserialize, Serialize};
use siren_bot::commands::fun::roll::{format_roll, parse_dice};
use siren_core::data::{ExecutableQuery, Value, condition::Condition, query::QueryBuilder};
use std::{fmt::Display, str::FromStr, sync::Arc};
use uuid::Uuid;

pub fn get_routes() -> Router<Arc<AppState>> {
  Router::new().route("/{guild_id}/track", post(add_track_dice))
}

const TABLE_NAME: &str = "dice_track";

#[derive(Serialize, Deserialize, Clone, Debug)]
enum TrackDiceOperator {
  #[serde(rename = "eq")]
  Equal,
  #[serde(rename = "lt")]
  LessThan,
  #[serde(rename = "lte")]
  LessThanEqual,
  #[serde(rename = "gt")]
  GreaterThan,
  #[serde(rename = "gte")]
  GreaterThanEqual,
}

// Implementing the ToString trait for converting the enum to a string
impl Display for TrackDiceOperator {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    let str = match self {
      TrackDiceOperator::Equal => "eq".to_string(),
      TrackDiceOperator::LessThan => "lt".to_string(),
      TrackDiceOperator::LessThanEqual => "lte".to_string(),
      TrackDiceOperator::GreaterThan => "gt".to_string(),
      TrackDiceOperator::GreaterThanEqual => "gte".to_string(),
    };
    write!(f, "{}", str)
  }
}

// Implementing the FromStr trait for parsing a string into the enum
impl FromStr for TrackDiceOperator {
  type Err = String;

  fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
    match s {
      "eq" => Ok(TrackDiceOperator::Equal),
      "lt" => Ok(TrackDiceOperator::LessThan),
      "lte" => Ok(TrackDiceOperator::LessThanEqual),
      "gt" => Ok(TrackDiceOperator::GreaterThan),
      "gte" => Ok(TrackDiceOperator::GreaterThanEqual),
      _ => Err(format!("Unknown value for TrackDiceOperator: {}", s)),
    }
  }
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DiceTrackPayload {
  dice: String,
  user_id: Option<i64>,
  value: Option<i32>,
  operator: Option<TrackDiceOperator>,
}

#[derive(Serialize, Deserialize, sqlx::FromRow, Clone, Debug)]
pub struct InsertDiceTrack {
  guild_id: i64,
  owner_id: i64,
  dice: String,
  user_id: Option<i64>,
  value: Option<i32>,
  operator: Option<String>,
}

#[derive(Serialize, Deserialize, sqlx::FromRow, Clone, Debug)]
pub struct QueryDiceTrack {
  id: Uuid,
  guild_id: i64,
  owner_id: i64,
  dice: String,
  user_id: Option<i64>,
  value: Option<i32>,
  operator: Option<String>,
}

impl QueryDiceTrack {
  pub async fn find(dice: &InsertDiceTrack) -> Option<Self> {
    QueryBuilder::new(TABLE_NAME)
      .where_condition(Condition::and(
        Condition::is_equal("guild_id", Value::BigInt(dice.guild_id)),
        Condition::and(
          Condition::is_equal("owner_id", Value::BigInt(dice.owner_id)),
          Condition::and(
            Condition::is_equal("dice", Value::Text(dice.dice.clone())),
            Condition::and(
              Condition::is_equal("user_id", Value::OptionalBigInt(dice.user_id)),
              Condition::and(
                Condition::is_equal("value", Value::OptionalInt(dice.value)),
                Condition::is_equal("operator", Value::OptionalText(dice.operator.clone())),
              ),
            ),
          ),
        ),
      ))
      .fetch_optional()
      .await
  }
}

impl InsertDiceTrack {
  pub async fn insert(&self) -> Result<QueryDiceTrack> {
    let pool = siren_core::data::pool();
    let query = format!(
      "INSERT INTO {} (
        guild_id,
        owner_id,
        dice,
        user_id,
        value,
        operator
      ) VALUES (
        $1, $2, $3, $4, $5, $6
      ) RETURNING *",
      TABLE_NAME
    );
    let item: QueryDiceTrack = match sqlx::query_as(&query)
      .bind(self.guild_id)
      .bind(self.owner_id)
      .bind(&self.dice)
      .bind(self.user_id)
      .bind(self.value)
      .bind(&self.operator)
      .fetch_optional(pool)
      .await?
    {
      Some(result) => result,
      None => return Err(Error::new(500, "Error storing".to_string())),
    };
    Ok(item)
  }
}

pub async fn add_track_dice(
  SessionAuthorization(session): SessionAuthorization,
  State(state): State<Arc<AppState>>,
  Path(guild_id): Path<u64>,
  Json(payload): Json<DiceTrackPayload>,
) -> Result<Json<QueryDiceTrack>> {
  let session = session.ok_or_else(|| Error::from(StatusCode::UNAUTHORIZED))?;

  // Resolve Discord snowflake for this local user and verify they exist in cache
  let discord_snowflake: u64 = {
    let pool = siren_core::data::pool();
    let pid: Option<String> = sqlx::query_scalar(
      "SELECT provider_user_id FROM user_connections \
       WHERE user_id = $1 AND provider = 'discord'",
    )
    .bind(session.user_id)
    .fetch_optional(pool)
    .await?;
    pid
      .and_then(|s| s.parse().ok())
      .ok_or_else(|| Error::not_found("Discord account not connected".to_string()))?
  };
  let owner_id = match state.cache.user(discord_snowflake) {
    Some(user) => user.id,
    None => {
      return Err(Error::not_found(
        "User not found in Discord cache".to_string(),
      ));
    }
  };

  // Validate if the guild exists in the cache
  let guild_id = match state.cache.guild(guild_id) {
    Some(guild) => guild.id,
    None => return Err(Error::not_found("Guild not found".to_string())),
  };

  let dice = parse_dice(&payload.dice)?;

  let insert_dice = InsertDiceTrack {
    guild_id: guild_id.get() as i64,
    owner_id: owner_id.get() as i64,
    dice: format_roll(dice.0, dice.1, dice.2),
    user_id: payload.user_id,
    value: payload.value,
    operator: payload.operator.map(|s| s.to_string()),
  };

  // Check for existing dice tracks
  let results = QueryDiceTrack::find(&insert_dice).await;

  match results {
    Some(dice_track) => Ok(Json(dice_track)),
    None => {
      let dice_track = insert_dice.insert().await?;
      Ok(Json(dice_track))
    }
  }
}
