use chrono::{DateTime, FixedOffset, Local, LocalResult, TimeZone};
use nu_protocol::{record, ShellError, Span, Value};

pub(crate) fn parse_date_from_string(
    input: &str,
    span: Span,
) -> Result<DateTime<FixedOffset>, Value> {
    match dtparse::parse(input) {
        Ok((native_dt, fixed_offset)) => {
            let offset = match fixed_offset {
                Some(offset) => offset,
                None => *(Local::now().offset()),
            };
            match offset.from_local_datetime(&native_dt) {
                LocalResult::Single(d) => Ok(d),
                LocalResult::Ambiguous(d, _) => Ok(d),
                LocalResult::None => Err(Value::error(
                    ShellError::DatetimeParseError {
                        msg: input.into(),
                        span,
                    },
                    span,
                )),
            }
        }
        Err(_) => Err(Value::error(
            ShellError::DatetimeParseError {
                msg: input.into(),
                span,
            },
            span,
        )),
    }
}

/// Generates a table containing available datetime format specifiers
///
/// # Arguments
/// * `head` - use the call's head
/// * `show_parse_only_formats` - whether parse-only format specifiers (that can't be outputted) should be shown. Should only be used for `into datetime`, not `format date`
pub(crate) fn generate_strftime_list(head: Span, show_parse_only_formats: bool) -> Value {
    let now = Local::now();

    struct FormatSpecification<'a> {
        spec: &'a str,
        description: &'a str,
    }

    let specifications = [
        FormatSpecification {
            spec: "%Y",
            description: "The full proleptic Gregorian year, zero-padded to 4 digits.",
        },
        FormatSpecification {
            spec: "%C",
            description: "The proleptic Gregorian year divided by 100, zero-padded to 2 digits.",
        },
        FormatSpecification {
            spec: "%y",
            description: "The proleptic Gregorian year modulo 100, zero-padded to 2 digits.",
        },
        FormatSpecification {
            spec: "%m",
            description: "Month number (01--12), zero-padded to 2 digits.",
        },
        FormatSpecification {
            spec: "%b",
            description: "Abbreviated month name. Always 3 letters.",
        },
        FormatSpecification {
            spec: "%B",
            description: "Full month name. Also accepts corresponding abbreviation in parsing.",
        },
        FormatSpecification {
            spec: "%h",
            description: "Same as %b.",
        },
        FormatSpecification {
            spec: "%d",
            description: "Day number (01--31), zero-padded to 2 digits.",
        },
        FormatSpecification {
            spec: "%e",
            description: "Same as %d but space-padded. Same as %_d.",
        },
        FormatSpecification {
            spec: "%a",
            description: "Abbreviated weekday name. Always 3 letters.",
        },
        FormatSpecification {
            spec: "%A",
            description: "Full weekday name. Also accepts corresponding abbreviation in parsing.",
        },
        FormatSpecification {
            spec: "%w",
            description: "Sunday = 0, Monday = 1, ..., Saturday = 6.",
        },
        FormatSpecification {
            spec: "%u",
            description: "Monday = 1, Tuesday = 2, ..., Sunday = 7. (ISO 8601)",
        },
        FormatSpecification {
            spec: "%U",
            description: "Week number starting with Sunday (00--53), zero-padded to 2 digits.",
        },
        FormatSpecification {
            spec: "%W",
            description:
                "Same as %U, but week 1 starts with the first Monday in that year instead.",
        },
        FormatSpecification {
            spec: "%G",
            description: "Same as %Y but uses the year number in ISO 8601 week date.",
        },
        FormatSpecification {
            spec: "%g",
            description: "Same as %y but uses the year number in ISO 8601 week date.",
        },
        FormatSpecification {
            spec: "%V",
            description: "Same as %U but uses the week number in ISO 8601 week date (01--53).",
        },
        FormatSpecification {
            spec: "%j",
            description: "Day of the year (001--366), zero-padded to 3 digits.",
        },
        FormatSpecification {
            spec: "%D",
            description: "Month-day-year format. Same as %m/%d/%y.",
        },
        FormatSpecification {
            spec: "%x",
            description: "Locale's date representation (e.g., 12/31/99).",
        },
        FormatSpecification {
            spec: "%F",
            description: "Year-month-day format (ISO 8601). Same as %Y-%m-%d.",
        },
        FormatSpecification {
            spec: "%v",
            description: "Day-month-year format. Same as %e-%b-%Y.",
        },
        FormatSpecification {
            spec: "%H",
            description: "Hour number (00--23), zero-padded to 2 digits.",
        },
        FormatSpecification {
            spec: "%k",
            description: "Same as %H but space-padded. Same as %_H.",
        },
        FormatSpecification {
            spec: "%I",
            description: "Hour number in 12-hour clocks (01--12), zero-padded to 2 digits.",
        },
        FormatSpecification {
            spec: "%l",
            description: "Same as %I but space-padded. Same as %_I.",
        },
        FormatSpecification {
            spec: "%P",
            description: "am or pm in 12-hour clocks.",
        },
        FormatSpecification {
            spec: "%p",
            description: "AM or PM in 12-hour clocks.",
        },
        FormatSpecification {
            spec: "%M",
            description: "Minute number (00--59), zero-padded to 2 digits.",
        },
        FormatSpecification {
            spec: "%S",
            description: "Second number (00--60), zero-padded to 2 digits.",
        },
        FormatSpecification {
            spec: "%f",
            description: "The fractional seconds (in nanoseconds) since last whole second.",
        },
        FormatSpecification {
            spec: "%.f",
            description: "Similar to .%f but left-aligned. These all consume the leading dot.",
        },
        FormatSpecification {
            spec: "%.3f",
            description: "Similar to .%f but left-aligned but fixed to a length of 3.",
        },
        FormatSpecification {
            spec: "%.6f",
            description: "Similar to .%f but left-aligned but fixed to a length of 6.",
        },
        FormatSpecification {
            spec: "%.9f",
            description: "Similar to .%f but left-aligned but fixed to a length of 9.",
        },
        FormatSpecification {
            spec: "%3f",
            description: "Similar to %.3f but without the leading dot.",
        },
        FormatSpecification {
            spec: "%6f",
            description: "Similar to %.6f but without the leading dot.",
        },
        FormatSpecification {
            spec: "%9f",
            description: "Similar to %.9f but without the leading dot.",
        },
        FormatSpecification {
            spec: "%R",
            description: "Hour-minute format. Same as %H:%M.",
        },
        FormatSpecification {
            spec: "%T",
            description: "Hour-minute-second format. Same as %H:%M:%S.",
        },
        FormatSpecification {
            spec: "%X",
            description: "Locale's time representation (e.g., 23:13:48).",
        },
        FormatSpecification {
            spec: "%r",
            description: "Hour-minute-second format in 12-hour clocks. Same as %I:%M:%S %p.",
        },
        FormatSpecification {
            spec: "%Z",
            description:
                "Local time zone name. Skips all non-whitespace characters during parsing.",
        },
        FormatSpecification {
            spec: "%z",
            description: "Offset from the local time to UTC (with UTC being +0000).",
        },
        FormatSpecification {
            spec: "%:z",
            description: "Same as %z but with a colon.",
        },
        FormatSpecification {
            spec: "%c",
            description: "Locale's date and time (e.g., Thu Mar 3 23:05:25 2005).",
        },
        FormatSpecification {
            spec: "%+",
            description: "ISO 8601 / RFC 3339 date & time format.",
        },
        FormatSpecification {
            spec: "%s",
            description: "UNIX timestamp, the number of seconds since 1970-01-01",
        },
        FormatSpecification {
            spec: "%t",
            description: "Literal tab (\\t).",
        },
        FormatSpecification {
            spec: "%n",
            description: "Literal newline (\\n).",
        },
        FormatSpecification {
            spec: "%%",
            description: "Literal percent sign.",
        },
    ];

    let mut records = specifications
        .iter()
        .map(|s| {
            Value::record(
                record! {
                    "Specification" => Value::string(s.spec, head),
                    "Example" => Value::string(now.format(s.spec).to_string(), head),
                    "Description" => Value::string(s.description, head),
                },
                head,
            )
        })
        .collect::<Vec<Value>>();

    if show_parse_only_formats {
        // now.format("%#z") will panic since it is parse-only
        // so here we emulate how it will look:
        let example = now
            .format("%:z") // e.g. +09:30
            .to_string()
            .get(0..3) // +09:30 -> +09
            .unwrap_or("")
            .to_string();

        let description = "Parsing only: Same as %z but allows minutes to be missing or present.";

        records.push(Value::record(
            record! {
                "Specification" => Value::string("%#z", head),
                "Example" => Value::string(example, head),
                "Description" => Value::string(description, head),
            },
            head,
        ));
    }

    Value::list(records, head)
}
