pub mod search;
pub mod types;

pub use search::{crawl, search, CrawlOptions};
pub use types::{
    AreaValue, Community, CommunityDetail, PriceRecord, PriceValue, Region, SaleListing,
    SearchListing, SearchParams,
};

use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, REFERER};
use tail_fin_common::TailFinError;
use types::{DetailResponse, HotResponse};

const USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
const HOT_URL: &str = "https://api.591.com.tw/api/community/rentHot";
const DETAIL_URL: &str = "https://api.591.com.tw/api/community/detail";

/// Taiwan region codes used by the 591 API.
pub const REGIONS: &[Region] = &[
    Region {
        id: 1,
        name: "台北市",
    },
    Region {
        id: 2,
        name: "新北市",
    },
    Region {
        id: 3,
        name: "桃園市",
    },
    Region {
        id: 4,
        name: "台中市",
    },
    Region {
        id: 5,
        name: "台南市",
    },
    Region {
        id: 6,
        name: "高雄市",
    },
    Region {
        id: 7,
        name: "基隆市",
    },
    Region {
        id: 8,
        name: "新竹市",
    },
    Region {
        id: 9,
        name: "嘉義市",
    },
    Region {
        id: 10,
        name: "新竹縣",
    },
    Region {
        id: 11,
        name: "苗栗縣",
    },
    Region {
        id: 12,
        name: "彰化縣",
    },
    Region {
        id: 13,
        name: "南投縣",
    },
    Region {
        id: 14,
        name: "雲林縣",
    },
    Region {
        id: 15,
        name: "嘉義縣",
    },
    Region {
        id: 16,
        name: "屏東縣",
    },
    Region {
        id: 17,
        name: "宜蘭縣",
    },
    Region {
        id: 18,
        name: "花蓮縣",
    },
    Region {
        id: 19,
        name: "台東縣",
    },
    Region {
        id: 20,
        name: "澎湖縣",
    },
    Region {
        id: 21,
        name: "金門縣",
    },
    Region {
        id: 22,
        name: "連江縣",
    },
];

/// Client for the 591 rental platform public API.
///
/// Uses native HTTP (reqwest) — no browser required.
pub struct Client591 {
    client: reqwest::Client,
}

impl Client591 {
    /// Create a new client.
    pub fn new() -> Result<Self, TailFinError> {
        let mut headers = HeaderMap::new();
        headers.insert(
            ACCEPT,
            HeaderValue::from_static("application/json, text/plain, */*"),
        );
        headers.insert(
            REFERER,
            HeaderValue::from_static("https://rent.591.com.tw/"),
        );

        let client = reqwest::Client::builder()
            .user_agent(USER_AGENT)
            .default_headers(headers)
            .build()
            .map_err(|e| TailFinError::Api(e.to_string()))?;

        Ok(Self { client })
    }

    /// Fetch the hot community list for a given region.
    ///
    /// `region_id` is the 591 region code (1 = Taipei City).
    /// Returns up to `limit` communities (the API ignores the limit param,
    /// so slicing is done client-side).
    pub async fn hot(&self, region_id: u32, limit: usize) -> Result<Vec<Community>, TailFinError> {
        let resp: HotResponse = self
            .client
            .get(HOT_URL)
            .query(&[("region_id", region_id.to_string())])
            .send()
            .await
            .map_err(|e| TailFinError::Api(e.to_string()))?
            .json()
            .await
            .map_err(|e| TailFinError::Parse(e.to_string()))?;

        if resp.status != 1 {
            return Err(TailFinError::Api(format!(
                "591 API returned status {}",
                resp.status
            )));
        }

        let mut items = resp.data.unwrap_or_default();
        items.truncate(limit);
        Ok(items)
    }

    /// Fetch detailed info for a community by ID.
    pub async fn community(&self, id: u64) -> Result<Option<CommunityDetail>, TailFinError> {
        let resp = self.fetch_detail(id).await?;
        Ok(resp.and_then(|d| d.community))
    }

    /// Fetch transaction price history for a community.
    ///
    /// Returns recent actual sale prices recorded in the ROC government registry.
    pub async fn price_history(
        &self,
        id: u64,
        limit: usize,
    ) -> Result<Vec<PriceRecord>, TailFinError> {
        let data = self.fetch_detail(id).await?;
        let mut records = data
            .and_then(|d| d.price)
            .map(|p| p.items)
            .unwrap_or_default();
        records.truncate(limit);
        Ok(records)
    }

    /// Fetch active sale listings near a community.
    ///
    /// Listings are flattened across all room types, sorted by post time (API order).
    pub async fn sales(
        &self,
        id: u64,
        limit: usize,
    ) -> Result<(u32, Vec<SaleListing>), TailFinError> {
        let data = self.fetch_detail(id).await?;
        let sale = data.and_then(|d| d.sale);
        let total = sale.as_ref().map(|s| s.total).unwrap_or(0);
        let mut listings: Vec<SaleListing> = sale
            .map(|s| s.rooms.into_iter().flat_map(|r| r.items).collect())
            .unwrap_or_default();
        listings.truncate(limit);
        Ok((total, listings))
    }

    // Shared detail fetch — one HTTP call, all sections.
    async fn fetch_detail(&self, id: u64) -> Result<Option<types::DetailData>, TailFinError> {
        let resp: DetailResponse = self
            .client
            .get(DETAIL_URL)
            .query(&[("id", id.to_string())])
            .send()
            .await
            .map_err(|e| TailFinError::Api(e.to_string()))?
            .json()
            .await
            .map_err(|e| TailFinError::Parse(e.to_string()))?;

        if resp.status != 1 {
            return Ok(None);
        }

        Ok(resp.data)
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{DetailResponse, HotResponse};

    #[test]
    fn test_client_new() {
        let client = Client591::new();
        assert!(client.is_ok());
    }

    #[test]
    fn test_hot_response_deserialize() {
        let json = r#"{"status":1,"msg":"請求成功","data":[{"id":"123","name":"Test"},{"id":"456","name":"Other"}]}"#;
        let resp: HotResponse = serde_json::from_str(json).unwrap();
        assert_eq!(resp.status, 1);
        let data = resp.data.unwrap();
        assert_eq!(data.len(), 2);
        assert_eq!(data[0].id, "123");
        assert_eq!(data[0].name, "Test");
    }

    #[test]
    fn test_hot_response_empty_data() {
        let json = r#"{"status":1,"msg":"請求成功","data":[]}"#;
        let resp: HotResponse = serde_json::from_str(json).unwrap();
        assert_eq!(resp.data.unwrap().len(), 0);
    }

    #[test]
    fn test_hot_response_error_status() {
        let json = r#"{"status":0,"msg":"error"}"#;
        let resp: HotResponse = serde_json::from_str(json).unwrap();
        assert_eq!(resp.status, 0);
        assert!(resp.data.is_none());
    }

    #[test]
    fn test_detail_response_deserialize() {
        let json = r#"{
            "status": 1,
            "data": {
                "community": {
                    "id": 7329,
                    "name": "台北晶麒",
                    "region": "台北市",
                    "section": "萬華區",
                    "address": "台北市萬華區康定路103號",
                    "age": "10年",
                    "floor": "26層",
                    "house_holds": "687戶",
                    "lat": "25.0387262",
                    "lng": "121.5013407",
                    "build_purpose": "住宅",
                    "base_area": "1124.00",
                    "const_company": "興富發建設股份有限公司",
                    "search_count": "248,275"
                },
                "price": { "items": [] },
                "sale": { "search_type": 1, "total": 0, "rooms": [] }
            }
        }"#;
        let resp: DetailResponse = serde_json::from_str(json).unwrap();
        assert_eq!(resp.status, 1);
        let data = resp.data.unwrap();
        let community = data.community.unwrap();
        assert_eq!(community.id, 7329);
        assert_eq!(community.name, "台北晶麒");
        assert_eq!(community.region.as_deref(), Some("台北市"));
    }

    #[test]
    fn test_price_record_deserialize() {
        let json = r#"{
            "id": 7238140,
            "date": "115-01-20",
            "address": "康定路103號 | 18樓之16",
            "layout": "1房1廳",
            "build_area": "20.00坪",
            "total_price": "1,490萬元",
            "unit_price": { "price": "74.5", "unit": "萬/坪" },
            "shift_floor": "18樓",
            "total_floor": "26樓",
            "build_purpose_str": "住宅"
        }"#;
        let record: PriceRecord = serde_json::from_str(json).unwrap();
        assert_eq!(record.id, 7238140);
        assert_eq!(record.date, "115-01-20");
        assert_eq!(record.unit_price.price, "74.5");
    }

    #[test]
    fn test_sale_listing_deserialize() {
        let json = r#"{
            "houseid": 20003249,
            "title": "台北晶麒景觀精緻宅",
            "price_v": { "price": "1,925", "unit": "萬" },
            "price_unit": "95.0萬/坪",
            "room": "1房1廳",
            "address": "萬華區-台北晶麒",
            "area_v": { "area": "27.83", "unit": "坪" },
            "floor": "19樓",
            "floor_en": "19F/26F",
            "photo_src": "https://img1.591.com.tw/test.jpg",
            "label": ["含車位", "有陽台"]
        }"#;
        let listing: SaleListing = serde_json::from_str(json).unwrap();
        assert_eq!(listing.houseid, 20003249);
        assert_eq!(listing.price_v.price, "1,925");
        assert_eq!(listing.label.len(), 2);
    }

    #[test]
    fn test_detail_response_not_found() {
        let json = r#"{"status":0,"msg":"[id]參數錯誤"}"#;
        let resp: DetailResponse = serde_json::from_str(json).unwrap();
        assert_eq!(resp.status, 0);
        assert!(resp.data.is_none());
    }

    #[test]
    fn test_community_serialize_skips_none() {
        let detail = CommunityDetail {
            id: 1,
            name: "Test".to_string(),
            region: Some("台北市".to_string()),
            section: None,
            address: None,
            age: None,
            floor: None,
            house_holds: None,
            lat: None,
            lng: None,
            build_purpose: None,
            base_area: None,
            const_company: None,
            search_count: None,
        };
        let json = serde_json::to_string(&detail).unwrap();
        assert!(json.contains("\"region\""));
        assert!(!json.contains("\"section\""));
    }

    #[test]
    fn test_regions_list() {
        assert_eq!(REGIONS.len(), 22);
        assert_eq!(REGIONS[0].id, 1);
        assert_eq!(REGIONS[0].name, "台北市");
        assert_eq!(REGIONS[5].id, 6);
        assert_eq!(REGIONS[5].name, "高雄市");
        // All IDs unique and sequential
        for (i, r) in REGIONS.iter().enumerate() {
            assert_eq!(r.id as usize, i + 1);
        }
    }

    #[tokio::test]
    async fn test_hot_live() {
        let client = Client591::new().unwrap();
        let result = client.hot(1, 5).await;
        assert!(result.is_ok(), "hot() failed: {:?}", result);
        let communities = result.unwrap();
        assert!(!communities.is_empty());
        assert!(communities.len() <= 5);
        for c in &communities {
            assert!(!c.id.is_empty());
            assert!(!c.name.is_empty());
        }
    }

    #[tokio::test]
    async fn test_community_live() {
        let client = Client591::new().unwrap();
        let result = client.community(7329).await;
        assert!(result.is_ok(), "community() failed: {:?}", result);
        let detail = result.unwrap().unwrap();
        assert_eq!(detail.id, 7329);
        assert!(detail.region.is_some());
        assert!(detail.address.is_some());
    }

    #[tokio::test]
    async fn test_price_history_live() {
        let client = Client591::new().unwrap();
        let result = client.price_history(7329, 5).await;
        assert!(result.is_ok(), "price_history() failed: {:?}", result);
        let records = result.unwrap();
        assert!(records.len() <= 5);
        if !records.is_empty() {
            assert!(!records[0].date.is_empty());
            assert!(!records[0].layout.is_empty());
            assert!(!records[0].total_price.is_empty());
        }
    }

    #[tokio::test]
    async fn test_sales_live() {
        let client = Client591::new().unwrap();
        let result = client.sales(7329, 5).await;
        assert!(result.is_ok(), "sales() failed: {:?}", result);
        let (total, listings) = result.unwrap();
        assert!(total > 0, "expected sale listings for 台北晶麒");
        assert!(listings.len() <= 5);
        if !listings.is_empty() {
            assert!(!listings[0].title.is_empty());
            assert!(!listings[0].price_v.price.is_empty());
        }
    }

    #[tokio::test]
    async fn test_community_not_found() {
        let client = Client591::new().unwrap();
        let result = client.community(0).await;
        assert!(result.is_ok());
        assert!(result.unwrap().is_none());
    }
}
