use std::str::FromStr;
use std::sync::Arc;

use axum::extract::Query;
use axum::http::StatusCode;
use axum::response::Json;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use tracing::{info, warn};

use crate::consts::{
    DEFAULT_PROPERTIES_LIMIT, ENUM_NULL, H3_PRECOMPUTE_MAX, H3_REQUEST_MAX, H3_REQUEST_MIN,
    MAX_PROPERTIES_LIMIT,
};
use crate::data::EnumFeatureData;
use crate::filter::{parse_filters, row_passes_filters};
use crate::state::AppState;

use super::parse::h3_cell_bounds;

#[derive(Deserialize)]
pub struct HexagonPropertiesParams {
    pub h3: String,
    pub resolution: u8,
    pub filters: Option<String>,
    pub limit: Option<usize>,
    pub offset: Option<usize>,
}

#[derive(Serialize)]
pub struct Property {
    // String fields
    pub address: Option<String>,
    pub postcode: Option<String>,
    pub property_type: Option<String>,
    pub built_form: Option<String>,
    pub duration: Option<String>,
    pub current_energy_rating: Option<String>,
    pub potential_energy_rating: Option<String>,

    // Numeric fields
    pub lat: f32,
    pub lon: f32,

    pub is_construction_date_approximate: Option<bool>,

    #[serde(flatten)]
    pub features: FxHashMap<String, f32>,
}

#[derive(Serialize)]
pub struct HexagonPropertiesResponse {
    pub properties: Vec<Property>,
    pub total: usize,
    pub limit: usize,
    pub offset: usize,
    pub truncated: bool,
}

fn non_empty_string(text: &str) -> Option<String> {
    let trimmed = text.trim();
    if trimmed.is_empty() {
        None
    } else {
        Some(trimmed.to_string())
    }
}

fn lookup_enum_value(
    enum_features: &[EnumFeatureData],
    enum_data: &[u8],
    num_enums: usize,
    enum_idx: &FxHashMap<String, usize>,
    row: usize,
    names: &[&str],
) -> Option<String> {
    for name in names {
        if let Some(&feature_index) = enum_idx.get(*name) {
            let enum_feature = &enum_features[feature_index];
            let data_index = enum_data[row * num_enums + feature_index];
            if data_index != ENUM_NULL {
                if let Some(value) = enum_feature.values.get(data_index as usize) {
                    return Some(value.clone());
                }
            }
        }
    }
    None
}

pub async fn get_hexagon_properties(
    state: Arc<AppState>,
    Query(params): Query<HexagonPropertiesParams>,
) -> Result<Json<HexagonPropertiesResponse>, (StatusCode, String)> {
    let cell = h3o::CellIndex::from_str(&params.h3).map_err(|error| {
        warn!(h3 = %params.h3, error = %error, "Invalid H3 cell index");
        (
            StatusCode::BAD_REQUEST,
            format!("Invalid H3 cell: {}", error),
        )
    })?;
    let cell_u64: u64 = cell.into();

    let resolution = params.resolution;
    if !(H3_REQUEST_MIN..=H3_REQUEST_MAX).contains(&resolution) {
        warn!(
            resolution,
            "Resolution out of range [{}, {}]", H3_REQUEST_MIN, H3_REQUEST_MAX
        );
        return Err((
            StatusCode::BAD_REQUEST,
            format!(
                "resolution must be between {} and {}",
                H3_REQUEST_MIN, H3_REQUEST_MAX
            ),
        ));
    }
    let h3_str = params.h3.clone();
    let filters_str = params.filters.clone();
    let (parsed_filters, parsed_enum_filters) = parse_filters(
        params.filters.as_deref(),
        &state.data.feature_names,
        &state.data.enum_features,
    );
    let num_filters = parsed_filters.len() + parsed_enum_filters.len();

    let result = tokio::task::spawn_blocking(move || {
        let t0 = std::time::Instant::now();
        let precomputed = &state.h3_cells;
        let h3_res = h3o::Resolution::try_from(resolution)
            .map_err(|err| format!("Invalid H3 resolution {}: {}", resolution, err))?;
        let need_parent = resolution < H3_PRECOMPUTE_MAX;
        let num_features = state.data.num_features;
        let num_enums = state.data.num_enums;
        let feature_data = &state.data.feature_data;
        let enum_data_flat = &state.data.enum_data;
        let enum_features = &state.data.enum_features;

        let (min_lat, min_lon, max_lat, max_lon) = h3_cell_bounds(cell, 0.001);

        let cell_for_row = |row: usize| -> u64 {
            let max_cell = precomputed[row];
            if !need_parent || max_cell == 0 {
                return max_cell;
            }
            h3o::CellIndex::try_from(max_cell)
                .ok()
                .and_then(|ci| ci.parent(h3_res))
                .map(u64::from)
                .unwrap_or(0)
        };

        let mut matching_rows: Vec<usize> = Vec::new();
        state
            .grid
            .for_each_in_bounds(min_lat, min_lon, max_lat, max_lon, |row_idx| {
                let row = row_idx as usize;
                if cell_for_row(row) == cell_u64
                    && row_passes_filters(
                        row,
                        &parsed_filters,
                        &parsed_enum_filters,
                        feature_data,
                        num_features,
                        enum_data_flat,
                        num_enums,
                    )
                {
                    matching_rows.push(row);
                }
            });

        let total = matching_rows.len();
        let limit = params
            .limit
            .unwrap_or(DEFAULT_PROPERTIES_LIMIT)
            .min(MAX_PROPERTIES_LIMIT);
        let offset = params.offset.unwrap_or(0);
        let truncated = total > offset + limit;

        let properties: Vec<Property> = matching_rows
            .iter()
            .skip(offset)
            .take(limit)
            .map(|&row| {
                let mut features = FxHashMap::default();
                let base = row * num_features;
                for (feat_idx, feat_name) in state.data.feature_names.iter().enumerate() {
                    let value = feature_data[base + feat_idx];
                    if value.is_finite() {
                        features.insert(feat_name.clone(), value);
                    }
                }

                Property {
                    address: non_empty_string(state.data.address(row)),
                    postcode: non_empty_string(state.data.postcode(row)),
                    is_construction_date_approximate: Some(state.data.is_approx_build_date(row)),
                    property_type: lookup_enum_value(
                        enum_features,
                        enum_data_flat,
                        num_enums,
                        &state.enum_name_to_idx,
                        row,
                        &["Property type", "epc_property_type", "pp_property_type"],
                    ),
                    built_form: lookup_enum_value(
                        enum_features,
                        enum_data_flat,
                        num_enums,
                        &state.enum_name_to_idx,
                        row,
                        &["Property type/built form", "built_form"],
                    ),
                    duration: lookup_enum_value(
                        enum_features,
                        enum_data_flat,
                        num_enums,
                        &state.enum_name_to_idx,
                        row,
                        &["Leashold/Freehold", "duration"],
                    ),
                    current_energy_rating: lookup_enum_value(
                        enum_features,
                        enum_data_flat,
                        num_enums,
                        &state.enum_name_to_idx,
                        row,
                        &["Current energy rating", "current_energy_rating"],
                    ),
                    potential_energy_rating: lookup_enum_value(
                        enum_features,
                        enum_data_flat,
                        num_enums,
                        &state.enum_name_to_idx,
                        row,
                        &["Potential energy rating", "potential_energy_rating"],
                    ),
                    lat: state.data.lat[row],
                    lon: state.data.lon[row],
                    features,
                }
            })
            .collect();

        let elapsed = t0.elapsed();
        info!(
            h3 = %h3_str,
            resolution,
            total,
            returned = properties.len(),
            offset,
            filters = num_filters,
            filters_raw = filters_str.as_deref().unwrap_or("-"),
            ms = format_args!("{:.1}", elapsed.as_secs_f64() * 1000.0),
            "GET /api/hexagon-properties"
        );

        Ok(HexagonPropertiesResponse {
            properties,
            total,
            limit,
            offset,
            truncated,
        })
    })
    .await
    .map_err(|error| (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()))?
    .map_err(|error: String| (StatusCode::INTERNAL_SERVER_ERROR, error))?;

    Ok(Json(result))
}
