use crate::error::Error;
use crate::function::Function;
use crate::runner::{Runnable, Runner};
use crate::writer::Writer;
use color_eyre::owo_colors::OwoColorize as _;
use eyre::Context;
use kinetics_api::func;
use serde_json::json;
use std::path::PathBuf;
use std::time::Duration;

#[derive(clap::Args, Clone)]
pub(crate) struct StatsCommand {
    /// Function name to get statistics for.
    /// Run `kinetics list` to get a complete list of function names in a project.
    #[arg()]
    name: String,

    /// Period to get statistics for.
    ///
    /// The period (e.g. `1day 3hours`) is a concatenation of time spans.
    /// Where each time span is an integer number and a suffix representing time units.
    ///
    /// Maximum available period is 7 days.
    /// Defaults to 1day.
    ///
    #[arg(short, long)]
    period: Option<String>,

    /// Relative path to the project directory
    #[arg(long)]
    project: Option<PathBuf>,
}

impl Runnable for StatsCommand {
    fn runner(&self, writer: &Writer) -> impl Runner {
        StatsRunner {
            command: self.clone(),
            writer,
        }
    }
}

struct StatsRunner<'a> {
    command: StatsCommand,
    writer: &'a Writer,
}

impl Runner for StatsRunner<'_> {
    /// Retrieves and displays run statistics for a specific function
    async fn run(&mut self) -> Result<(), Error> {
        let project = self.project(&self.command.project).await?;

        // Get all function names without any additional manipulations.
        let all_functions = project.functions()?;
        let function = Function::find_by_name(&all_functions, &self.command.name)?;
        let client = self.api_client().await?;

        self.writer.text(&format!(
            "\n{} {} {}...\n\n",
            console::style("Fetching stats").bold().green(),
            console::style("for").dim(),
            console::style(&function.name).bold()
        ))?;

        let response = client
            .post("/function/stats")
            .json(&func::stats::Request {
                project: (&project).into(),
                function_name: function.name,
                period: self.command.period.to_owned(),
            })
            .send()
            .await
            .wrap_err("Failed to send request to stat endpoint")?;

        if !response.status().is_success() {
            let status = response.status();
            let error_text = response.text().await.unwrap_or("Unknown error".to_string());

            log::error!(
                "Failed to fetch statistics from API ({}): {}",
                status,
                error_text
            );

            return Err(Error::new(
                "Failed to fetch statistics",
                Some("Try again later."),
            ));
        }

        let stats_response: func::stats::Response = response.json().await.wrap_err(Error::new(
            "Invalid response from server",
            Some("Try again later."),
        ))?;

        // Period returned by the API in human-readable format
        let period = stats_response.period.clone();

        self.writer
            .text(&format!("{} {}\n", "Period:".bold(), period))?;

        self.writer.text(&format!(
            "{}\n  Total: {}\n  Success: {}\n  Error: {}\n",
            "Runs:".bold(),
            stats_response.runs.total,
            stats_response.runs.success,
            stats_response.runs.error,
        ))?;

        self.writer.json(json!({
            "success": true,
            "runs": &stats_response.runs,
            "queue": &stats_response.queue,
            "period": period,
        }))?;

        if let Some(queue) = stats_response.queue {
            self.writer.text(&format!(
                "\n{}\n  Waiting: {}\n  Oldest: {}\n  In flight: {}\n  Retries: {}\n  Failed: {}\n  Completed: {}\n",
                "Queue:".bold(),
                queue.waiting,
                humantime::format_duration(Duration::from_secs(queue.oldest.round() as u64)),
                queue.in_flight,
                queue.retries,
                queue.failed,
                queue.completed,
            ))?;
        }

        Ok(())
    }
}
