//! Lease deals for phase-0 flats โ€” soft3 local (this browser), no backend.
//!
//! A flat's colour on the map is its status. Two sources decide it: the lease
//! book (`terms.rs`, synced from the plot sheet โ€” holder, type, price) and a
//! deal stored here. A signed or live deal wins; otherwise the book speaks.

use crate::orders::PublishedDeal;
use crate::terms::{bare_name, terms, LandUse};
use crate::land::load_map;
use crate::wallet::{load_intents, save_intents};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

pub const DEALS_KEY: &str = "cyberia.deals.v2";
/// v1 deals were opened by merely visiting /plot/<id>/lease โ€” not by a
/// request โ€” so they are dropped on load, with their journal lines.
const LEGACY_DEALS_KEY: &str = "cyberia.deals.v1";

/// Where a flat stands inside its land use. The colour on the map is the
/// land use (`terms::LandUse`); this is what you can do with the flat.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FlatStatus {
    /// residence / business / dual use, priced and free
    Available,
    /// Avalon joint venture, unpriced business ground, a whole-district HGB sale
    OnRequest,
    /// a live deal in this browser
    Transfer,
    Owned,
    /// city land, no price, or no land use set
    Closed,
}

impl FlatStatus {
    pub fn label(self) -> &'static str {
        match self {
            FlatStatus::Available => "AVAILABLE",
            FlatStatus::OnRequest => "ON REQUEST",
            FlatStatus::Transfer => "IN TRANSFER",
            FlatStatus::Owned => "OWNED",
            FlatStatus::Closed => "NOT ON OFFER",
        }
    }
}

/// Deal steps in order. The last one closes the deal โ€” the flat becomes owned.
pub const STEPS: &[(&str, &str)] = &[
    ("request", "lease requested โ€” the flat is spoken for"),
    ("review", "council review โ€” plot, price, neighbours"),
    ("docs", "documents โ€” passport, agreement, survey"),
    ("transfer", "rights in transfer โ€” signatures on both sides"),
    ("signed", "signed โ€” the flat is yours"),
];

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DealEvent {
    pub step: String,
    pub ts_ms: f64,
    pub note: String,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Deal {
    pub flat_id: String,
    pub flat_name: String,
    pub step: String,
    #[serde(default)]
    pub cancelled: bool,
    #[serde(default)]
    pub history: Vec<DealEvent>,
    /// the valley published it (`/plot-data`) โ€” never saved to this browser
    #[serde(default)]
    pub published: Option<PublishedDeal>,
}

impl Deal {
    pub fn is_signed(&self) -> bool {
        !self.cancelled && self.step == "signed"
    }

    pub fn is_live(&self) -> bool {
        !self.cancelled && !self.is_signed()
    }

    pub fn step_index(&self) -> usize {
        STEPS.iter().position(|(s, _)| *s == self.step).unwrap_or(0)
    }

    pub fn step_line(&self) -> &'static str {
        STEPS
            .iter()
            .find(|(s, _)| *s == self.step)
            .map(|(_, line)| *line)
            .unwrap_or("")
    }
}

pub fn now_ms() -> f64 {
    js_sys::Date::now()
}

/// `2026-09-24 12:31` in the browser's local time.
pub fn fmt_ts(ms: f64) -> String {
    let d = js_sys::Date::new(&ms.into());
    format!(
        "{:04}-{:02}-{:02} {:02}:{:02}",
        d.get_full_year(),
        d.get_month() + 1,
        d.get_date(),
        d.get_hours(),
        d.get_minutes()
    )
}

pub fn load_deals() -> HashMap<String, Deal> {
    let Some(ls) = web_sys::window().and_then(|w| w.local_storage().ok().flatten()) else {
        return HashMap::new();
    };
    if let Some(raw) = ls.get_item(LEGACY_DEALS_KEY).ok().flatten() {
        let legacy: HashMap<String, Deal> = serde_json::from_str(&raw).unwrap_or_default();
        let mut q = load_intents();
        q.retain(|i| !(i.fleet == "YOU" && i.action == "lease" && legacy.contains_key(&i.flat)));
        save_intents(&q);
        let _ = ls.remove_item(LEGACY_DEALS_KEY);
    }
    ls.get_item(DEALS_KEY)
        .ok()
        .flatten()
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_default()
}

pub fn save_deals(m: &HashMap<String, Deal>) {
    let own: HashMap<&String, &Deal> = m.iter().filter(|(_, d)| d.published.is_none()).collect();
    if let Some(ls) = web_sys::window().and_then(|w| w.local_storage().ok().flatten()) {
        if let Ok(s) = serde_json::to_string(&own) {
            let _ = ls.set_item(DEALS_KEY, &s);
        }
    }
}

/// The valley's published deals outrank this browser's own track: reserved
/// and transfer paint IN TRANSFER, owned paints OWNED, available clears.
pub fn merge_published(deals: &mut HashMap<String, Deal>, index: &HashMap<String, PublishedDeal>) {
    let map = load_map();
    for (id, pd) in index {
        let step = match pd.status.as_str() {
            "reserved" | "transfer" => "transfer",
            "owned" => "signed",
            _ => {
                if deals.get(id).map(|d| d.published.is_some()).unwrap_or(false) {
                    deals.remove(id);
                }
                continue;
            }
        };
        let name = map
            .phase0
            .iter()
            .find(|f| &f.id == id)
            .map(|f| bare_name(&f.name))
            .unwrap_or_else(|| id.to_uppercase());
        let own_history = deals.get(id).map(|d| d.history.clone()).unwrap_or_default();
        let mut history = own_history;
        history.push(DealEvent {
            step: step.into(),
            ts_ms: now_ms(),
            note: format!("published by the valley ยท {}", pd.line()),
        });
        deals.insert(
            id.clone(),
            Deal {
                flat_id: id.clone(),
                flat_name: name,
                step: step.into(),
                cancelled: false,
                history,
                published: Some(pd.clone()),
            },
        );
    }
}

/// Who holds the flat, per the lease book (the city excluded).
pub fn holder(id: &str) -> Option<&'static str> {
    terms(id).and_then(|t| t.holder())
}

pub fn flat_status(id: &str, deals: &HashMap<String, Deal>) -> FlatStatus {
    match deals.get(id) {
        Some(d) if d.is_signed() => return FlatStatus::Owned,
        Some(d) if d.is_live() => return FlatStatus::Transfer,
        _ => {}
    }
    let Some(t) = terms(id) else {
        return FlatStatus::Closed;
    };
    if t.holder().is_some() {
        return FlatStatus::Owned;
    }
    if t.in_reregistration() {
        return FlatStatus::Closed;
    }
    let priced_and_free = !t.is_city_land() && t.price().is_some();
    match t.land_use() {
        // Avalon is one special project โ€” joint venture only, whatever the price
        LandUse::Venture if id.starts_with("avalon") => FlatStatus::OnRequest,
        LandUse::Lease | LandUse::Dual | LandUse::Venture if priced_and_free => FlatStatus::Available,
        // on sale with the price agreed on request
        LandUse::Lease | LandUse::Dual if !t.is_city_land() => FlatStatus::Available,
        // business ground with no price yet, the city's construction business
        LandUse::Venture => FlatStatus::OnRequest,
        LandUse::Hgb if !t.is_city_land() => FlatStatus::OnRequest,
        _ => FlatStatus::Closed,
    }
}

/// Start a deal at `request`. A cancelled deal restarts from scratch.
pub fn open_deal(deals: &mut HashMap<String, Deal>, id: &str, name: &str) {
    let fresh = Deal {
        flat_id: id.to_string(),
        flat_name: name.to_string(),
        step: "request".into(),
        cancelled: false,
        history: vec![DealEvent {
            step: "request".into(),
            ts_ms: now_ms(),
            note: "lease requested on the flat page".into(),
        }],
        published: None,
    };
    match deals.get_mut(id) {
        Some(d) if !d.cancelled => {}
        _ => {
            deals.insert(id.to_string(), fresh);
        }
    }
    save_deals(deals);
}

pub fn advance_deal(deals: &mut HashMap<String, Deal>, id: &str, note: &str) {
    if let Some(d) = deals.get_mut(id) {
        let i = d.step_index();
        if d.cancelled || d.published.is_some() || i + 1 >= STEPS.len() {
            return;
        }
        d.step = STEPS[i + 1].0.into();
        d.history.push(DealEvent {
            step: d.step.clone(),
            ts_ms: now_ms(),
            note: note.into(),
        });
    }
    save_deals(deals);
}

pub fn cancel_deal(deals: &mut HashMap<String, Deal>, id: &str, note: &str) {
    if let Some(d) = deals.get_mut(id) {
        if d.is_signed() || d.published.is_some() {
            return;
        }
        d.cancelled = true;
        d.history.push(DealEvent {
            step: "cancelled".into(),
            ts_ms: now_ms(),
            note: note.into(),
        });
    }
    save_deals(deals);
}

/// Due-diligence requests on flats that are not yours: flat id โ†’ when asked.
pub const DD_KEY: &str = "cyberia.dd.v1";

pub fn load_dd() -> HashMap<String, f64> {
    web_sys::window()
        .and_then(|w| w.local_storage().ok().flatten())
        .and_then(|ls| ls.get_item(DD_KEY).ok().flatten())
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_default()
}

pub fn request_dd(m: &mut HashMap<String, f64>, id: &str) {
    m.entry(id.to_string()).or_insert_with(now_ms);
    if let Some(ls) = web_sys::window().and_then(|w| w.local_storage().ok().flatten()) {
        if let Ok(s) = serde_json::to_string(m) {
            let _ = ls.set_item(DD_KEY, &s);
        }
    }
}

/// Requests for on-request land: flat id (or `district:<zone>` for a whole
/// HGB district) โ†’ when asked.
pub const JV_KEY: &str = "cyberia.jv.v1";

pub fn load_jv() -> HashMap<String, f64> {
    web_sys::window()
        .and_then(|w| w.local_storage().ok().flatten())
        .and_then(|ls| ls.get_item(JV_KEY).ok().flatten())
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_default()
}

pub fn request_jv(m: &mut HashMap<String, f64>, id: &str) {
    m.entry(id.to_string()).or_insert_with(now_ms);
    if let Some(ls) = web_sys::window().and_then(|w| w.local_storage().ok().flatten()) {
        if let Ok(s) = serde_json::to_string(m) {
            let _ = ls.set_item(JV_KEY, &s);
        }
    }
}

Graph