//! Price a CDR and print the `price::Report` in various tables.

use std::{io::IsTerminal as _, path::PathBuf};

use chrono_tz::Tz;
use clap::{Args, Parser};
use console::style;
use ocpi_tariffs::{cdr, json, price, tariff, timezone, warning::VerdictExt as _};

use crate::{
    load_object_file, load_object_from_stdin,
    opts::{job_description, FilePath, Version},
    print::{self, Col, Table},
    Error, ObjectKind,
};

/// The arguments and logic for the `price::Command`.
#[derive(Parser)]
pub struct Command {
    /// Arguments for the `price::Command`.
    #[command(flatten)]
    args: Arguments,
}

/// Arguments for the price command.
#[derive(Args)]
struct Arguments {
    /// A path to the charge detail record in JSON format.
    ///
    /// If no path is provided the CDR is read from standard in.
    #[arg(short = 'c', long)]
    cdr: Option<PathBuf>,

    /// A path to the tariff structure in JSON format.
    ///
    /// If no path is provided, then the tariff is inferred to be contained
    /// inside the provided CDR. If the CDR contains multiple tariff structures,
    /// the first valid tariff will be used.
    #[arg(short = 't', long)]
    tariff: Option<PathBuf>,

    /// Timezone for evaluating any local times contained in the tariff structure.
    #[arg(short = 'z', long, alias = "tz")]
    timezone: Option<String>,

    /// The OCPI version that should be used for the input structures.
    ///
    /// If the input consists of version 2.1.1 structures they will be converted to 2.2.1
    /// structures. The actual calculation and output will always be according to OCPI 2.2.1.
    ///
    /// Use `detect` to let to tool try to find the matching version.
    #[arg(short = 'o', long, value_enum)]
    ocpi_version: Version,
}

impl Command {
    /// Run the `price` Command.
    pub fn run(self) -> Result<(), Error> {
        let Self {
            args:
                Arguments {
                    cdr,
                    tariff,
                    timezone,
                    ocpi_version,
                },
        } = self;

        let ocpi_version = ocpi_version.into();
        let cdr_file_path = cdr.as_deref().map(FilePath::from_path).transpose()?;
        let tariff_file_path = tariff.as_deref().map(FilePath::from_path).transpose()?;

        let job_description = job_description(cdr_file_path.as_ref(), tariff_file_path.as_ref());
        eprintln!("{} {}", style("Analyzing").green().bold(), job_description);

        let cdr_json = if let Some(path) = cdr_file_path.as_deref() {
            load_object_file(path, ObjectKind::Cdr)?
        } else if std::io::stdin().is_terminal() {
            return Err(Error::CdrRequired);
        } else {
            load_object_from_stdin(ObjectKind::Cdr)?
        };
        let cdr_doc = json::parse_object(&cdr_json)?;
        let (cdr, cdr_warnings) = cdr::build(cdr_doc, ocpi_version).into_parts();

        let timezone = if let Some(tz) = timezone {
            tz.parse().map_err(Error::InvalidTimezone)?
        } else {
            let timezone = match timezone::find_or_infer(&cdr) {
                Ok(tz) => tz,
                Err(err_set) => {
                    let (error, warnings) = err_set.into_parts();
                    print::timezone_error(&error);
                    print::warning_set("the timezone search", &warnings);
                    return Err(Error::Handled);
                }
            };
            let (timezone_source, warnings) = timezone.into_parts();

            print::warning_set("CDR validation", &cdr_warnings);
            print::warning_set("the timezone search", &warnings);

            timezone_source.into_timezone()
        };

        let report = if let Some(path) = tariff_file_path {
            let tariff_json = load_object_file(&path, ObjectKind::Tariff)?;
            let tariff_doc = json::parse_object(&tariff_json)?;
            let (tariff, tariff_warnings) = tariff::build(tariff_doc, ocpi_version).into_parts();
            print::warning_set("tariff validation", &tariff_warnings);
            cdr::price(&cdr, price::TariffSource::Override(vec![tariff]), timezone).only_error()?
        } else {
            cdr::price(&cdr, price::TariffSource::UseCdr, timezone).only_error()?
        };

        let (report, warnings) = report.into_parts();

        eprintln!(
            "Completed {} {}, using time-zone `{}`:\n",
            style("analyzing").green().bold(),
            job_description,
            style(&timezone).blue(),
        );

        print::warning_set("pricing the CDR", &warnings);
        print::tariff_reports(&report.tariff_reports);
        let timezone: Tz = report.timezone.parse().map_err(Error::InvalidTimezone)?;

        {
            let table = periods_summary(&report, timezone);
            println!("{table}");
        }

        {
            let (outcome, table) = compare::totals(&report);
            println!("{table}");

            match outcome {
                compare::Outcome::AllValid => {
                    eprintln!(
                        "Pricing report {} all totals in the CDR.",
                        style("matches").green().bold()
                    );
                }
                compare::Outcome::SomeInvalid => {
                    eprintln!(
                        "Pricing report does {} totals in the CDR.",
                        style("not match").red().bold()
                    );
                }
            }
        }

        Ok(())
    }
}

/// Return a table summarizing the periods of a CDR [`price::Report`].
#[must_use]
fn periods_summary(report: &price::Report, timezone: Tz) -> String {
    let price::Report {
        periods,
        tariff_used,
        tariff_reports: _,
        timezone: _,
        billed_charging_time: _,
        billed_energy: _,
        billed_idle_time: _,
        total_charging_time: _,
        total_energy,
        total_idle_time,
        total_time,
        total_cost: _,
        total_energy_cost,
        total_fixed_cost,
        total_idle_cost,
        total_charging_time_cost,
    } = report;

    let currency = tariff_used.currency;

    let mut table = Table::header(&[
        Col {
            label: &style("Period").green(),
            width: 25,
        },
        Col::empty(8),
        Col {
            label: &style("Energy").green(),
            width: 10,
        },
        Col {
            label: &style("Charging Time").green(),
            width: 13,
        },
        Col {
            label: &style("Parking Time").green(),
            width: 13,
        },
        Col {
            label: &style("Flat").green(),
            width: 10,
        },
    ]);

    for period in periods {
        let start_time = period.start_date_time.with_timezone(&timezone);
        let dim = &period.dimensions;

        table.print_row(&[
            &start_time,
            &style("Volume").green(),
            &dim.energy.as_ref().map(|d| d.volume).unwrap_or_default(),
            &dim.duration_charging
                .as_ref()
                .map(|d| d.volume)
                .unwrap_or_default(),
            &dim.duration_idle
                .as_ref()
                .map(|d| d.volume)
                .unwrap_or_default(),
            &dim.flat
                .price
                .as_ref()
                .map(|_| "x".to_owned())
                .unwrap_or_default(),
        ]);

        table.print_row(&[
            &"",
            &style("Price").green(),
            &dim.energy
                .as_ref()
                .and_then(|d| d.price.as_ref())
                .map(price::Component::price)
                .unwrap_or_default()
                .display_currency(currency),
            &dim.duration_charging
                .as_ref()
                .and_then(|d| d.price.as_ref())
                .map(price::Component::price)
                .unwrap_or_default()
                .display_currency(currency),
            &dim.duration_idle
                .as_ref()
                .and_then(|d| d.price.as_ref())
                .map(price::Component::price)
                .unwrap_or_default()
                .display_currency(currency),
            &dim.flat
                .price
                .as_ref()
                .map(price::Component::price)
                .unwrap_or_default()
                .display_currency(currency),
        ]);
    }

    table.print_line();

    table.print_row(&[
        &style("Total").green(),
        &style("Volume").green(),
        &print::Optional(total_energy.calculated),
        &total_time.calculated,
        &print::Optional(total_idle_time.calculated),
        &"",
    ]);

    table.print_row(&[
        &"",
        &style("Price").green(),
        &total_energy_cost
            .calculated
            .map(|p| p.excl_vat)
            .unwrap_or_default()
            .display_currency(currency),
        &total_charging_time_cost
            .calculated
            .map(|p| p.excl_vat)
            .unwrap_or_default()
            .display_currency(currency),
        &total_idle_cost
            .calculated
            .map(|p| p.excl_vat)
            .unwrap_or_default()
            .display_currency(currency),
        &total_fixed_cost
            .calculated
            .map(|p| p.excl_vat)
            .unwrap_or_default()
            .display_currency(currency),
    ]);

    table.finish()
}

mod compare {
    //! Compare the fields of a [`price::Report`].
    use console::style;
    use ocpi_tariffs::{price, Kwh};

    use crate::print::{self, Col, Table};

    /// The outcome of calling the `totals` function.
    #[derive(Copy, Clone)]
    pub(super) enum Outcome {
        /// All [`price::Report`] fields are valid.
        AllValid,

        /// Some [`price::Report`] fields are invalid.
        SomeInvalid,
    }

    /// Compare the `total_*` fields in the [`price::Report`].
    ///
    /// Return a `Outcome` signaling if any of the fields are invalid and
    /// the text of a table detailing the comparisons performed.
    #[must_use]
    pub(super) fn totals(report: &price::Report) -> (Outcome, String) {
        let price::Report {
            periods: _,
            tariff_used,
            tariff_reports: _,
            timezone: _,
            billed_charging_time: _,
            billed_energy: _,
            billed_idle_time: _,
            total_charging_time: _,
            total_energy,
            total_idle_time,
            total_time,
            total_cost,
            total_energy_cost,
            total_fixed_cost,
            total_idle_cost,
            total_charging_time_cost: total_time_cost,
        } = report;

        let currency = tariff_used.currency;

        let mut all_totals_valid = true;

        let mut table = Table::header(&[
            Col {
                label: &style("Property").green(),
                width: 28,
            },
            Col {
                label: &style("Report").green(),
                width: 10,
            },
            Col {
                label: &style("CDR").green(),
                width: 10,
            },
        ]);

        {
            let valid = total_time.cdr == total_time.calculated;

            table.print_valid_row(
                valid,
                "Total Time",
                &[&total_time.calculated, &total_time.cdr],
            );

            all_totals_valid &= valid;
        }

        {
            let valid = total_idle_time.cdr == total_idle_time.calculated;

            table.print_valid_row(
                valid,
                "Total Parking Time",
                &[
                    &print::Optional(total_idle_time.calculated),
                    &total_idle_time.cdr.unwrap_or_default(),
                ],
            );

            all_totals_valid &= valid;
        }

        {
            let valid = total_energy
                .calculated
                .map(|kwh| kwh == total_energy.cdr)
                .unwrap_or(true);

            table.print_valid_row(
                valid,
                "Total Energy",
                &[
                    &print::Optional(total_energy.calculated.map(Kwh::rescale)),
                    &total_energy.cdr,
                ],
            );

            all_totals_valid &= valid;
        }

        {
            let valid = total_cost
                .calculated
                .map(|p| p == total_cost.cdr)
                .unwrap_or(true);

            table.print_valid_row(
                valid,
                "Total Cost (Excl.)",
                &[
                    &total_cost
                        .calculated
                        .map(|p| p.excl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                    &total_cost.cdr.rescale().excl_vat.display_currency(currency),
                ],
            );

            table.print_valid_row(
                valid,
                "Total Cost (Incl.)",
                &[
                    &total_cost
                        .calculated
                        .and_then(|p| p.incl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                    &total_cost
                        .cdr
                        .incl_vat
                        .unwrap_or_default()
                        .display_currency(currency),
                ],
            );

            all_totals_valid &= valid;
        }

        {
            let valid = total_time_cost
                .calculated
                .zip(total_time_cost.cdr)
                .map(|(l, r)| l == r)
                .unwrap_or(true);

            table.print_valid_row(
                valid,
                "Total Time Cost (Excl.)",
                &[
                    &total_time_cost
                        .calculated
                        .map(|p| p.rescale().excl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                    &total_time_cost
                        .cdr
                        .map(|p| p.excl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                ],
            );

            table.print_valid_row(
                valid,
                "Total Time Cost (Incl.)",
                &[
                    &total_time_cost
                        .calculated
                        .and_then(|p| p.rescale().incl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                    &total_time_cost
                        .cdr
                        .and_then(|p| p.incl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                ],
            );

            all_totals_valid &= valid;
        }

        {
            let valid = total_fixed_cost
                .calculated
                .zip(total_fixed_cost.cdr)
                .map(|(l, r)| l == r)
                .unwrap_or(true);

            table.print_valid_row(
                valid,
                "Total Fixed Cost (Excl.)",
                &[
                    &total_fixed_cost
                        .calculated
                        .map(|p| p.excl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                    &total_fixed_cost
                        .cdr
                        .map(|p| p.excl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                ],
            );

            table.print_valid_row(
                valid,
                "Total Fixed Cost (Incl.)",
                &[
                    &total_fixed_cost
                        .calculated
                        .and_then(|p| p.incl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                    &total_fixed_cost
                        .cdr
                        .and_then(|p| p.incl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                ],
            );

            all_totals_valid &= valid;
        }

        {
            let valid = total_energy_cost
                .calculated
                .zip(total_energy_cost.cdr)
                .map(|(l, r)| l == r)
                .unwrap_or(true);

            table.print_valid_row(
                valid,
                "Total Energy Cost (Excl.)",
                &[
                    &total_energy_cost
                        .calculated
                        .map(|p| p.excl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                    &total_energy_cost
                        .cdr
                        .map(|p| p.excl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                ],
            );

            table.print_valid_row(
                valid,
                "Total Energy Cost (Incl.)",
                &[
                    &total_energy_cost
                        .calculated
                        .and_then(|p| p.incl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                    &total_energy_cost
                        .cdr
                        .and_then(|p| p.incl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                ],
            );

            all_totals_valid &= valid;
        }

        {
            let valid = total_idle_cost
                .calculated
                .zip(total_idle_cost.cdr)
                .map(|(l, r)| l == r)
                .unwrap_or(true);

            table.print_valid_row(
                valid,
                "Total Idle Cost (Excl.)",
                &[
                    &total_idle_cost
                        .calculated
                        .map(|p| p.excl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                    &total_idle_cost
                        .cdr
                        .map(|p| p.excl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                ],
            );

            table.print_valid_row(
                valid,
                "Total Idle Cost (Incl.)",
                &[
                    &total_idle_cost
                        .calculated
                        .and_then(|p| p.incl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                    &total_idle_cost
                        .cdr
                        .and_then(|p| p.incl_vat)
                        .unwrap_or_default()
                        .display_currency(currency),
                ],
            );

            all_totals_valid &= valid;
        }

        let outcome = if all_totals_valid {
            Outcome::AllValid
        } else {
            Outcome::SomeInvalid
        };

        (outcome, table.finish())
    }
}
