use std::{cmp::Ordering, collections::BTreeMap};

use sora_diagnostics::{Result, SoraError};
use sora_ir::model::{ConfigIr, DerivedFieldIr, FieldIr, StructIr, TableIr, TypeIr};

use crate::model::{ConfigData, RowData, Value};

pub fn materialize_derived_fields(ir: &ConfigIr, data: &ConfigData) -> Result<ConfigData> {
    let mut materialized = data.clone();

    for table in &ir.tables {
        for field in table
            .fields
            .iter()
            .filter(|field| field.derived_from.is_some())
        {
            materialize_table_derived_field(ir, data, &mut materialized, table, field)?;
        }
    }

    Ok(materialized)
}

fn materialize_table_derived_field(
    ir: &ConfigIr,
    source_data: &ConfigData,
    materialized: &mut ConfigData,
    parent_table: &TableIr,
    field: &FieldIr,
) -> Result<()> {
    let derived_from = field
        .derived_from
        .as_ref()
        .expect("caller filters to derived fields");
    let shape = derived_field_shape(ir, field)?;
    let Some(parent_data) = materialized
        .tables
        .iter_mut()
        .find(|table| table.name == parent_table.name)
    else {
        return Ok(());
    };
    let source_rows = source_data
        .tables
        .iter()
        .find(|table| table.name == derived_from.source_table)
        .map(|table| table.rows.as_slice())
        .unwrap_or(&[]);

    for parent_row in &mut parent_data.rows {
        let parent_key = parent_row
            .values
            .get(&derived_from.parent_key)
            .ok_or_else(|| SoraError::MissingRequiredField {
                table: parent_table.name.clone(),
                field: derived_from.parent_key.clone(),
            })?;
        let mut child_rows = matching_child_rows(source_rows, derived_from, parent_key)?;
        if let Some(order_by) = &derived_from.order_by {
            child_rows.sort_by(|left, right| compare_order_field(left, right, order_by));
        }

        let values = child_rows
            .into_iter()
            .map(|row| derive_child_value(&derived_from.source_table, row, &shape.value))
            .collect::<Result<Vec<_>>>()?;
        let value = match shape.cardinality {
            DerivedFieldCardinality::List => Value::List(values),
            DerivedFieldCardinality::RequiredOne => {
                if values.len() != 1 {
                    return Err(derived_field_row_count_error(
                        parent_table,
                        field,
                        derived_from,
                        parent_key,
                        "exactly 1",
                        values.len(),
                    ));
                }
                values.into_iter().next().expect("checked one value")
            }
            DerivedFieldCardinality::OptionalOne => {
                if values.len() > 1 {
                    return Err(derived_field_row_count_error(
                        parent_table,
                        field,
                        derived_from,
                        parent_key,
                        "at most 1",
                        values.len(),
                    ));
                }
                values.into_iter().next().unwrap_or(Value::Null)
            }
        };
        parent_row.values.insert(field.name.clone(), value);
    }

    Ok(())
}

struct DerivedFieldShape<'a> {
    cardinality: DerivedFieldCardinality,
    value: DerivedFieldValue<'a>,
}

#[derive(Debug, Clone, Copy)]
enum DerivedFieldCardinality {
    List,
    RequiredOne,
    OptionalOne,
}

enum DerivedFieldValue<'a> {
    Struct(&'a StructIr),
    Field(&'a str),
}

fn derived_field_shape<'a>(ir: &'a ConfigIr, field: &'a FieldIr) -> Result<DerivedFieldShape<'a>> {
    let derived_from = field
        .derived_from
        .as_ref()
        .expect("caller filters to derived fields");
    let (cardinality, value_ty) = match &field.ty {
        TypeIr::List(element) => (DerivedFieldCardinality::List, element.as_ref()),
        TypeIr::Optional(element) => (DerivedFieldCardinality::OptionalOne, element.as_ref()),
        ty => (DerivedFieldCardinality::RequiredOne, ty),
    };

    if let Some(value_field) = &derived_from.value_field {
        return Ok(DerivedFieldShape {
            cardinality,
            value: DerivedFieldValue::Field(value_field),
        });
    }

    let TypeIr::Struct(struct_name) = value_ty else {
        return Err(SoraError::InvalidSchema(format!(
            "derived field `{}` must assemble struct values or declare `from.field`",
            field.name
        )));
    };

    let struct_ir = ir
        .structs
        .iter()
        .find(|item| item.name == *struct_name)
        .ok_or_else(|| {
            SoraError::InvalidSchema(format!(
                "derived field `{}` references unknown struct `{struct_name}`",
                field.name
            ))
        })?;

    Ok(DerivedFieldShape {
        cardinality,
        value: DerivedFieldValue::Struct(struct_ir),
    })
}

fn matching_child_rows<'a>(
    source_rows: &'a [RowData],
    derived_from: &DerivedFieldIr,
    parent_key: &Value,
) -> Result<Vec<&'a RowData>> {
    let mut rows = Vec::new();
    for row in source_rows {
        let Some(child_key) = row.values.get(&derived_from.child_key) else {
            return Err(SoraError::MissingRequiredField {
                table: derived_from.source_table.clone(),
                field: derived_from.child_key.clone(),
            });
        };
        if stable_key(child_key) == stable_key(parent_key) {
            rows.push(row);
        }
    }
    Ok(rows)
}

fn derive_struct_value(source_table: &str, row: &RowData, struct_ir: &StructIr) -> Result<Value> {
    let mut values = BTreeMap::new();
    for field in &struct_ir.fields {
        if let Some(value) = row.values.get(&field.name) {
            values.insert(field.name.clone(), value.clone());
        } else if field.is_required() {
            return Err(SoraError::MissingRequiredField {
                table: source_table.to_owned(),
                field: field.name.clone(),
            });
        }
    }
    Ok(Value::Object(values))
}

fn derive_child_value(
    source_table: &str,
    row: &RowData,
    value: &DerivedFieldValue<'_>,
) -> Result<Value> {
    match value {
        DerivedFieldValue::Struct(struct_ir) => derive_struct_value(source_table, row, struct_ir),
        DerivedFieldValue::Field(field) => {
            row.values
                .get(*field)
                .cloned()
                .ok_or_else(|| SoraError::MissingRequiredField {
                    table: source_table.to_owned(),
                    field: (*field).to_owned(),
                })
        }
    }
}

fn derived_field_row_count_error(
    parent_table: &TableIr,
    field: &FieldIr,
    derived_from: &DerivedFieldIr,
    parent_key: &Value,
    expected: &'static str,
    actual: usize,
) -> SoraError {
    SoraError::InvalidSchema(format!(
        "derived field `{}` in table `{}` expected {} row from `{}` where `{}` = `{}`, but found {}",
        field.name,
        parent_table.name,
        expected,
        derived_from.source_table,
        derived_from.child_key,
        stable_key(parent_key),
        actual
    ))
}

fn compare_order_field(left: &RowData, right: &RowData, order_by: &str) -> Ordering {
    let left = left.values.get(order_by);
    let right = right.values.get(order_by);
    compare_optional_values(left, right)
}

fn compare_optional_values(left: Option<&Value>, right: Option<&Value>) -> Ordering {
    match (left, right) {
        (Some(left), Some(right)) => compare_values(left, right),
        (None, Some(_)) => Ordering::Less,
        (Some(_), None) => Ordering::Greater,
        (None, None) => Ordering::Equal,
    }
}

fn compare_values(left: &Value, right: &Value) -> Ordering {
    match (left, right) {
        (Value::Bool(left), Value::Bool(right)) => left.cmp(right),
        (Value::Integer(left), Value::Integer(right)) => left.cmp(right),
        (Value::Float(left), Value::Float(right)) => {
            left.partial_cmp(right).unwrap_or(Ordering::Equal)
        }
        (Value::String(left), Value::String(right)) => left.cmp(right),
        _ => stable_key(left).cmp(&stable_key(right)),
    }
}

fn stable_key(value: &Value) -> String {
    match value {
        Value::Bool(value) => value.to_string(),
        Value::Integer(value) => value.to_string(),
        Value::Float(value) => value.to_string(),
        Value::String(value) => value.clone(),
        Value::List(_) => "<list>".to_owned(),
        Value::Object(_) => "<object>".to_owned(),
        Value::Null => "<null>".to_owned(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::TableData;
    use sora_ir::{normalize::normalize_schema, validate::validate_config_ir};
    use sora_schema::model::SchemaFile;

    #[test]
    fn materializes_child_rows_into_parent_list_field() {
        let ir = derived_field_ir();
        let data = ConfigData {
            tables: vec![
                TableData {
                    name: "Item".to_owned(),
                    rows: vec![RowData {
                        values: BTreeMap::from([
                            ("id".to_owned(), Value::Integer(1001)),
                            ("name".to_owned(), Value::String("Iron Sword".to_owned())),
                        ]),
                    }],
                },
                TableData {
                    name: "ItemReward".to_owned(),
                    rows: vec![
                        RowData {
                            values: BTreeMap::from([
                                ("item_id".to_owned(), Value::Integer(1001)),
                                ("seq".to_owned(), Value::Integer(2)),
                                ("reward_item_id".to_owned(), Value::Integer(3002)),
                                ("count".to_owned(), Value::Integer(5)),
                            ]),
                        },
                        RowData {
                            values: BTreeMap::from([
                                ("item_id".to_owned(), Value::Integer(1001)),
                                ("seq".to_owned(), Value::Integer(1)),
                                ("reward_item_id".to_owned(), Value::Integer(3001)),
                                ("count".to_owned(), Value::Integer(2)),
                            ]),
                        },
                    ],
                },
            ],
        };

        let materialized = materialize_derived_fields(&ir, &data).unwrap();
        let rewards = &materialized.tables[0].rows[0].values["rewards"];

        assert_eq!(
            rewards,
            &Value::List(vec![
                Value::Object(BTreeMap::from([
                    ("count".to_owned(), Value::Integer(2)),
                    ("reward_item_id".to_owned(), Value::Integer(3001)),
                ])),
                Value::Object(BTreeMap::from([
                    ("count".to_owned(), Value::Integer(5)),
                    ("reward_item_id".to_owned(), Value::Integer(3002)),
                ])),
            ])
        );
    }

    #[test]
    fn materializes_single_child_value_field() {
        let ir = single_value_derived_field_ir("string");
        let data = ConfigData {
            tables: vec![
                TableData {
                    name: "Item".to_owned(),
                    rows: vec![RowData {
                        values: BTreeMap::from([("id".to_owned(), Value::Integer(1001))]),
                    }],
                },
                TableData {
                    name: "ItemProfile".to_owned(),
                    rows: vec![RowData {
                        values: BTreeMap::from([
                            ("item_id".to_owned(), Value::Integer(1001)),
                            ("name".to_owned(), Value::String("Iron Sword".to_owned())),
                            ("notes".to_owned(), Value::String("ignored".to_owned())),
                        ]),
                    }],
                },
            ],
        };

        let materialized = materialize_derived_fields(&ir, &data).unwrap();

        assert_eq!(
            materialized.tables[0].rows[0].values["display_name"],
            Value::String("Iron Sword".to_owned())
        );
    }

    #[test]
    fn materializes_missing_optional_child_value_as_null() {
        let ir = single_value_derived_field_ir("optional<string>");
        let data = ConfigData {
            tables: vec![
                TableData {
                    name: "Item".to_owned(),
                    rows: vec![RowData {
                        values: BTreeMap::from([("id".to_owned(), Value::Integer(1001))]),
                    }],
                },
                TableData {
                    name: "ItemProfile".to_owned(),
                    rows: Vec::new(),
                },
            ],
        };

        let materialized = materialize_derived_fields(&ir, &data).unwrap();

        assert_eq!(
            materialized.tables[0].rows[0].values["display_name"],
            Value::Null
        );
    }

    #[test]
    fn rejects_missing_required_single_child_value() {
        let ir = single_value_derived_field_ir("string");
        let data = ConfigData {
            tables: vec![
                TableData {
                    name: "Item".to_owned(),
                    rows: vec![RowData {
                        values: BTreeMap::from([("id".to_owned(), Value::Integer(1001))]),
                    }],
                },
                TableData {
                    name: "ItemProfile".to_owned(),
                    rows: Vec::new(),
                },
            ],
        };

        let error = materialize_derived_fields(&ir, &data).unwrap_err();

        assert!(
            error
                .to_string()
                .contains("expected exactly 1 row from `ItemProfile`")
        );
    }

    #[test]
    fn rejects_multiple_single_child_values() {
        let ir = single_value_derived_field_ir("optional<string>");
        let data = ConfigData {
            tables: vec![
                TableData {
                    name: "Item".to_owned(),
                    rows: vec![RowData {
                        values: BTreeMap::from([("id".to_owned(), Value::Integer(1001))]),
                    }],
                },
                TableData {
                    name: "ItemProfile".to_owned(),
                    rows: vec![
                        RowData {
                            values: BTreeMap::from([
                                ("item_id".to_owned(), Value::Integer(1001)),
                                ("name".to_owned(), Value::String("Iron Sword".to_owned())),
                            ]),
                        },
                        RowData {
                            values: BTreeMap::from([
                                ("item_id".to_owned(), Value::Integer(1001)),
                                ("name".to_owned(), Value::String("Sword".to_owned())),
                            ]),
                        },
                    ],
                },
            ],
        };

        let error = materialize_derived_fields(&ir, &data).unwrap_err();

        assert!(
            error
                .to_string()
                .contains("expected at most 1 row from `ItemProfile`")
        );
    }

    fn derived_field_ir() -> ConfigIr {
        let schema: SchemaFile = toml::from_str(
            r#"
package = "game_config"

[[structs]]
name = "Reward"

[[structs.fields]]
name = "reward_item_id"
type = "i32"

[[structs.fields]]
name = "count"
type = "i32"

[[tables]]
name = "Item"
mode = "map"
key = "id"

[[tables.fields]]
name = "id"
type = "i32"

[[tables.fields]]
name = "name"
type = "string"

[[tables.fields]]
name = "rewards"
type = "list<Reward>"
from = { table = "ItemReward", parent_key = "id", child_key = "item_id", order_by = "seq" }

[[tables]]
name = "ItemReward"
mode = "list"

[[tables.fields]]
name = "item_id"
type = "i32"

[[tables.fields]]
name = "seq"
type = "i32"

[[tables.fields]]
name = "reward_item_id"
type = "i32"

[[tables.fields]]
name = "count"
type = "i32"
"#,
        )
        .unwrap();
        let ir = normalize_schema(schema).unwrap();
        validate_config_ir(&ir).unwrap();
        ir
    }

    fn single_value_derived_field_ir(field_type: &str) -> ConfigIr {
        let schema: SchemaFile = toml::from_str(&format!(
            r#"
package = "game_config"

[[tables]]
name = "Item"
mode = "map"
key = "id"

[[tables.fields]]
name = "id"
type = "i32"

[[tables.fields]]
name = "display_name"
type = "{field_type}"
from = {{ table = "ItemProfile", parent_key = "id", child_key = "item_id", field = "name" }}

[[tables]]
name = "ItemProfile"
mode = "list"

[[tables.fields]]
name = "item_id"
type = "i32"

[[tables.fields]]
name = "name"
type = "string"

[[tables.fields]]
name = "notes"
type = "string"
"#
        ))
        .unwrap();
        let ir = normalize_schema(schema).unwrap();
        validate_config_ir(&ir).unwrap();
        ir
    }
}
