use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;

// =============================================================================
// Core Entities (synced from league API)
// =============================================================================

#[derive(Debug, FromRow, Serialize, Deserialize)]
pub struct Team {
    pub id: i64,
    pub abbrev: String,
    pub short_name: String,
    pub long_name: String,
    pub season: i64,
    pub manager1_name: Option<String>,
    pub manager2_name: Option<String>,
    pub gm_discord_id: Option<String>,
    pub gm2_discord_id: Option<String>,
    pub division_id: Option<i64>,
    pub division_name: Option<String>,
    pub league_abbrev: Option<String>,
    pub thumbnail: Option<String>,
    pub color: Option<String>,
    pub dice_color: Option<String>,
    pub stadium: Option<String>,
    pub salary_cap: Option<f64>,
    pub synced_at: Option<NaiveDateTime>,
}

#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
pub struct Player {
    pub id: i64,
    pub name: String,
    pub season: i64,
    pub team_id: Option<i64>,
    pub swar: Option<f64>,
    pub card_image: Option<String>,
    pub card_image_alt: Option<String>,
    pub headshot: Option<String>,
    pub vanity_card: Option<String>,
    pub pos_1: Option<String>,
    pub pos_2: Option<String>,
    pub pos_3: Option<String>,
    pub pos_4: Option<String>,
    pub pos_5: Option<String>,
    pub pos_6: Option<String>,
    pub pos_7: Option<String>,
    pub pos_8: Option<String>,
    pub hand: Option<String>,
    pub injury_rating: Option<String>,
    pub il_return: Option<String>,
    pub demotion_week: Option<i64>,
    pub strat_code: Option<String>,
    pub bbref_id: Option<String>,
    pub sbaplayer_id: Option<i64>,
    pub last_game: Option<String>,
    pub last_game2: Option<String>,
    pub synced_at: Option<NaiveDateTime>,
}

impl Player {
    pub fn positions(&self) -> Vec<&str> {
        [
            self.pos_1.as_deref(),
            self.pos_2.as_deref(),
            self.pos_3.as_deref(),
            self.pos_4.as_deref(),
            self.pos_5.as_deref(),
            self.pos_6.as_deref(),
            self.pos_7.as_deref(),
            self.pos_8.as_deref(),
        ]
        .into_iter()
        .flatten()
        .collect()
    }

    pub fn is_pitcher(&self) -> bool {
        self.positions()
            .iter()
            .any(|p| matches!(*p, "SP" | "RP" | "CP"))
    }

    pub fn is_batter(&self) -> bool {
        self.positions()
            .iter()
            .any(|p| matches!(*p, "C" | "1B" | "2B" | "3B" | "SS" | "LF" | "CF" | "RF" | "DH"))
    }
}

// =============================================================================
// Card Data (imported from Strat-o-Matic)
// =============================================================================

#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
pub struct BatterCard {
    pub id: i64,
    pub player_id: i64,
    // vs Left-Handed Pitchers
    pub so_vlhp: f64,
    pub bb_vlhp: f64,
    pub hit_vlhp: f64,
    pub ob_vlhp: f64,
    pub tb_vlhp: f64,
    pub hr_vlhp: f64,
    pub dp_vlhp: f64,
    // vs Right-Handed Pitchers
    pub so_vrhp: f64,
    pub bb_vrhp: f64,
    pub hit_vrhp: f64,
    pub ob_vrhp: f64,
    pub tb_vrhp: f64,
    pub hr_vrhp: f64,
    pub dp_vrhp: f64,
    // Ballpark modifiers
    pub bphr_vlhp: f64,
    pub bphr_vrhp: f64,
    pub bp1b_vlhp: f64,
    pub bp1b_vrhp: f64,
    // Running game
    pub stealing: Option<String>,
    pub steal_rating: Option<String>,
    pub speed: Option<i64>,
    // Batting extras
    pub bunt: Option<String>,
    pub hit_run: Option<String>,
    // Fielding
    pub fielding: Option<String>,
    // Catcher-specific
    pub catcher_arm: Option<i64>,
    pub catcher_pb: Option<i64>,
    pub catcher_t: Option<i64>,
    // Computed ratings
    pub rating_vl: Option<f64>,
    pub rating_vr: Option<f64>,
    pub rating_overall: Option<f64>,
    // Import metadata
    pub imported_at: Option<NaiveDateTime>,
    pub source: Option<String>,
}

#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
pub struct PitcherCard {
    pub id: i64,
    pub player_id: i64,
    // vs Left-Handed Batters
    pub so_vlhb: f64,
    pub bb_vlhb: f64,
    pub hit_vlhb: f64,
    pub ob_vlhb: f64,
    pub tb_vlhb: f64,
    pub hr_vlhb: f64,
    pub dp_vlhb: f64,
    pub bphr_vlhb: f64,
    pub bp1b_vlhb: f64,
    // vs Right-Handed Batters
    pub so_vrhb: f64,
    pub bb_vrhb: f64,
    pub hit_vrhb: f64,
    pub ob_vrhb: f64,
    pub tb_vrhb: f64,
    pub hr_vrhb: f64,
    pub dp_vrhb: f64,
    pub bphr_vrhb: f64,
    pub bp1b_vrhb: f64,
    // Pitcher attributes
    pub hold_rating: Option<i64>,
    pub endurance_start: Option<i64>,
    pub endurance_relief: Option<i64>,
    pub endurance_close: Option<i64>,
    pub fielding_range: Option<i64>,
    pub fielding_error: Option<i64>,
    pub wild_pitch: Option<i64>,
    pub balk: Option<i64>,
    pub batting_rating: Option<String>,
    // Computed ratings
    pub rating_vlhb: Option<f64>,
    pub rating_vrhb: Option<f64>,
    pub rating_overall: Option<f64>,
    // Import metadata
    pub imported_at: Option<NaiveDateTime>,
    pub source: Option<String>,
}

// =============================================================================
// Transactions (synced from league API)
// =============================================================================

#[derive(Debug, FromRow, Serialize, Deserialize)]
pub struct Transaction {
    pub id: i64,
    pub season: i64,
    pub week: i64,
    pub move_id: String,
    pub player_id: i64,
    pub from_team_id: i64,
    pub to_team_id: i64,
    pub cancelled: bool,
    pub frozen: bool,
    pub synced_at: Option<NaiveDateTime>,
}

// =============================================================================
// Roster (aggregate view, not a DB table)
// =============================================================================

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Roster {
    pub majors: Vec<Player>,
    pub minors: Vec<Player>,
    pub il: Vec<Player>,
}

// =============================================================================
// User Data (local only)
// =============================================================================

#[derive(Debug, FromRow, Serialize, Deserialize)]
pub struct Lineup {
    pub id: i64,
    pub name: String,
    pub description: Option<String>,
    pub lineup_type: String,
    pub batting_order: Option<String>, // JSON string
    pub positions: Option<String>,     // JSON string
    pub starting_pitcher_id: Option<i64>,
    pub created_at: Option<NaiveDateTime>,
    pub updated_at: Option<NaiveDateTime>,
}

impl Lineup {
    pub fn batting_order_vec(&self) -> Vec<i64> {
        self.batting_order
            .as_deref()
            .and_then(|s| serde_json::from_str(s).ok())
            .unwrap_or_default()
    }

    pub fn positions_map(&self) -> std::collections::HashMap<String, i64> {
        self.positions
            .as_deref()
            .and_then(|s| serde_json::from_str(s).ok())
            .unwrap_or_default()
    }

    pub fn set_batting_order(&mut self, order: &[i64]) {
        self.batting_order = serde_json::to_string(order).ok();
    }

    pub fn set_positions(&mut self, positions: &std::collections::HashMap<String, i64>) {
        self.positions = serde_json::to_string(positions).ok();
    }
}

#[derive(Debug, FromRow, Serialize, Deserialize)]
pub struct MatchupCache {
    pub id: i64,
    pub batter_id: i64,
    pub pitcher_id: i64,
    pub rating: f64,
    pub tier: Option<String>,
    pub details: Option<String>, // JSON string
    pub computed_at: Option<NaiveDateTime>,
    pub weights_hash: Option<String>,
}

#[derive(Debug, FromRow, Serialize, Deserialize)]
pub struct StandardizedScoreCache {
    pub id: i64,
    pub batter_card_id: Option<i64>,
    pub pitcher_card_id: Option<i64>,
    pub split: String,
    pub total_score: f64,
    pub stat_scores: String, // JSON string
    pub computed_at: Option<NaiveDateTime>,
    pub weights_hash: Option<String>,
    pub league_stats_hash: Option<String>,
}

#[derive(Debug, FromRow, Serialize, Deserialize)]
pub struct SyncStatus {
    pub id: i64,
    pub entity_type: String,
    pub last_sync: Option<NaiveDateTime>,
    pub last_sync_count: Option<i64>,
    pub last_error: Option<String>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;

    fn make_player(pos_1: Option<&str>, pos_2: Option<&str>) -> Player {
        Player {
            id: 1,
            name: "Test Player".to_string(),
            season: 13,
            team_id: Some(1),
            swar: None,
            card_image: None,
            card_image_alt: None,
            headshot: None,
            vanity_card: None,
            pos_1: pos_1.map(|s| s.to_string()),
            pos_2: pos_2.map(|s| s.to_string()),
            pos_3: None,
            pos_4: None,
            pos_5: None,
            pos_6: None,
            pos_7: None,
            pos_8: None,
            hand: None,
            injury_rating: None,
            il_return: None,
            demotion_week: None,
            strat_code: None,
            bbref_id: None,
            sbaplayer_id: None,
            last_game: None,
            last_game2: None,
            synced_at: None,
        }
    }

    #[test]
    fn player_positions_collects_non_none() {
        let p = make_player(Some("SS"), Some("2B"));
        assert_eq!(p.positions(), vec!["SS", "2B"]);
    }

    #[test]
    fn player_positions_skips_none() {
        let p = make_player(Some("CF"), None);
        assert_eq!(p.positions(), vec!["CF"]);
    }

    #[test]
    fn player_is_pitcher_true_for_sp() {
        assert!(make_player(Some("SP"), None).is_pitcher());
    }

    #[test]
    fn player_is_pitcher_true_for_rp_in_pos2() {
        assert!(make_player(Some("DH"), Some("RP")).is_pitcher());
    }

    #[test]
    fn player_is_pitcher_false_for_batter() {
        assert!(!make_player(Some("SS"), None).is_pitcher());
    }

    #[test]
    fn player_is_batter_true_for_position_players() {
        for pos in &["C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "DH"] {
            assert!(make_player(Some(pos), None).is_batter(), "{} should be a batter", pos);
        }
    }

    #[test]
    fn player_is_batter_false_for_pitcher() {
        assert!(!make_player(Some("SP"), None).is_batter());
    }

    fn make_lineup() -> Lineup {
        Lineup {
            id: 1,
            name: "Test Lineup".to_string(),
            description: None,
            lineup_type: "standard".to_string(),
            batting_order: None,
            positions: None,
            starting_pitcher_id: None,
            created_at: None,
            updated_at: None,
        }
    }

    #[test]
    fn lineup_batting_order_empty_when_none() {
        let l = make_lineup();
        assert!(l.batting_order_vec().is_empty());
    }

    #[test]
    fn lineup_batting_order_roundtrip() {
        let mut l = make_lineup();
        let order = vec![101, 102, 103, 104, 105, 106, 107, 108, 109];
        l.set_batting_order(&order);
        assert_eq!(l.batting_order_vec(), order);
    }

    #[test]
    fn lineup_positions_empty_when_none() {
        let l = make_lineup();
        assert!(l.positions_map().is_empty());
    }

    #[test]
    fn lineup_positions_roundtrip() {
        let mut l = make_lineup();
        let mut positions = HashMap::new();
        positions.insert("SS".to_string(), 101);
        positions.insert("CF".to_string(), 102);
        l.set_positions(&positions);
        assert_eq!(l.positions_map(), positions);
    }

    #[test]
    fn lineup_batting_order_handles_invalid_json() {
        let mut l = make_lineup();
        l.batting_order = Some("not valid json".to_string());
        assert!(l.batting_order_vec().is_empty());
    }

    #[test]
    fn lineup_positions_handles_invalid_json() {
        let mut l = make_lineup();
        l.positions = Some("{bad}".to_string());
        assert!(l.positions_map().is_empty());
    }
}
