use crate::error::{Error, Result};
use regex::Regex;
use std::io::{Cursor, Read};

const NEMWEB_BASE: &str = "https://nemweb.com.au";
const MMSDM_BASE: &str = "https://nemweb.com.au/Data_Archive/Wholesale_Electricity/MMSDM";

/// Real-time report types available on NEMWeb.
#[derive(Clone, Copy)]
pub enum RealtimeReport {
    /// DispatchIS — contains DISPATCH PRICE, REGIONSUM, INTERCONNECTORRES.
    Dispatch,
    /// TradingIS — contains TRADING PRICE.
    Trading,
    /// PredispatchIS — contains PREDISPATCH PRICE / REGION_PRICES.
    Predispatch,
    /// Dispatch_SCADA — contains DISPATCH UNIT_SCADA.
    Scada,
}

impl RealtimeReport {
    fn dir_url(self) -> &'static str {
        match self {
            Self::Dispatch => "https://nemweb.com.au/Reports/Current/DispatchIS_Reports/",
            Self::Trading => "https://nemweb.com.au/Reports/Current/TradingIS_Reports/",
            Self::Predispatch => "https://nemweb.com.au/Reports/Current/PredispatchIS_Reports/",
            Self::Scada => "https://nemweb.com.au/Reports/Current/Dispatch_SCADA/",
        }
    }

    fn zip_pattern(self) -> &'static str {
        match self {
            Self::Dispatch => "PUBLIC_DISPATCHIS_",
            Self::Trading => "PUBLIC_TRADINGIS_",
            Self::Predispatch => "PUBLIC_PREDISPATCHIS_",
            Self::Scada => "PUBLIC_DISPATCHSCADA_",
        }
    }
}

/// MMSDM archive dataset types.
#[derive(Clone, Copy)]
pub enum MmsdmDataset {
    DispatchPrice,
    TradingPrice,
    DispatchRegionSum,
    DispatchInterconnectorRes,
    DispatchUnitScada,
}

impl MmsdmDataset {
    /// A regex fragment matching this dataset's zip file name in the
    /// MMSDM archive directory listing.
    ///
    /// AEMO changed the MMSDM file naming convention in August 2024:
    /// * Old (≤ 2024-07): `PUBLIC_DVD_<TABLE>_YYYYMM010000.zip`
    /// * New (≥ 2024-08): `PUBLIC_ARCHIVE#<TABLE>#FILE01#YYYYMM010000.zip`
    ///   (the `#` is percent-encoded as `%23` in the href attribute)
    ///
    /// The pattern matches both. We rely on the leading `DVD_` or
    /// `%23` and the trailing `_` or `%23` to act as word boundaries,
    /// so e.g. `DISPATCHPRICE` does not accidentally match
    /// `PREDISPATCHPRICE` or `DISPATCHPRICESENSITIVITIES`.
    fn zip_pattern(self) -> &'static str {
        match self {
            Self::DispatchPrice => "DVD_DISPATCHPRICE_|%23DISPATCHPRICE%23",
            Self::TradingPrice => "DVD_TRADINGPRICE_|%23TRADINGPRICE%23",
            Self::DispatchRegionSum => "DVD_DISPATCHREGIONSUM_|%23DISPATCHREGIONSUM%23",
            Self::DispatchInterconnectorRes => {
                "DVD_DISPATCHINTERCONNECTORRES_|%23DISPATCHINTERCONNECTORRES%23"
            }
            Self::DispatchUnitScada => "DVD_DISPATCH_UNIT_SCADA_|%23DISPATCH_UNIT_SCADA%23",
        }
    }
}

/// Download a zip from a URL, extract the first CSV file, return its content.
async fn download_and_extract(client: &reqwest::Client, url: &str) -> Result<String> {
    let bytes = client
        .get(url)
        .send()
        .await?
        .error_for_status()?
        .bytes()
        .await?;
    let cursor = Cursor::new(bytes);
    let mut archive = zip::ZipArchive::new(cursor)?;
    let mut file = archive.by_index(0)?;
    let mut csv_text = String::new();
    file.read_to_string(&mut csv_text)?;
    Ok(csv_text)
}

/// Find the latest zip file matching a pattern in an AEMO directory listing.
///
/// `pattern` may contain regex alternation (e.g. `a|b`) — it is wrapped in a
/// non-capturing group so operator precedence works correctly.
fn find_latest_zip(html: &str, pattern: &str, base_url: &str) -> Result<String> {
    let re = Regex::new(&format!(r#"(?i)href="([^"]*(?:{pattern})[^"]*\.zip)""#))?;
    let mut files: Vec<&str> = re
        .captures_iter(html)
        .filter_map(|c| c.get(1).map(|m| m.as_str()))
        .collect();
    files.sort();
    let latest = files
        .last()
        .ok_or_else(|| Error::NotFound(format!("No zip files matching '{pattern}' found")))?;
    if latest.starts_with("http://") || latest.starts_with("https://") {
        Ok(latest.to_string())
    } else if latest.starts_with('/') {
        Ok(format!("{NEMWEB_BASE}{latest}"))
    } else {
        let base = base_url.trim_end_matches('/');
        Ok(format!("{base}/{latest}"))
    }
}

/// Fetch the latest real-time CSV for a given report type.
pub async fn fetch_realtime_csv(
    client: &reqwest::Client,
    report: RealtimeReport,
) -> Result<String> {
    let dir_url = report.dir_url();
    let html = client
        .get(dir_url)
        .send()
        .await?
        .error_for_status()?
        .text()
        .await?;
    let url = find_latest_zip(&html, report.zip_pattern(), dir_url)?;
    download_and_extract(client, &url).await
}

/// Fetch an MMSDM archive CSV for a given dataset/year/month.
pub async fn fetch_mmsdm_csv(
    client: &reqwest::Client,
    year: u32,
    month: u32,
    dataset: MmsdmDataset,
) -> Result<String> {
    let dir_url = format!(
        "{MMSDM_BASE}/{year}/MMSDM_{year}_{month:02}/MMSDM_Historical_Data_SQLLoader/DATA/"
    );
    let resp = client.get(&dir_url).send().await?;
    // 404 means the month directory isn't published yet — treat as "not
    // available" so historical queries skip it. 403 is propagated because it
    // typically signals rate limiting, not missing data.
    if resp.status() == reqwest::StatusCode::NOT_FOUND {
        return Err(Error::NotFound(format!(
            "MMSDM data not available for {year}-{month:02}"
        )));
    }
    let html = resp.error_for_status()?.text().await?;
    let url = find_latest_zip(&html, dataset.zip_pattern(), &dir_url)?;
    download_and_extract(client, &url).await
}

/// List available months in MMSDM archive for a given year.
pub async fn list_mmsdm_months(client: &reqwest::Client, year: u32) -> Result<Vec<u32>> {
    let url = format!("{MMSDM_BASE}/{year}/");
    let html = client
        .get(&url)
        .send()
        .await?
        .error_for_status()?
        .text()
        .await?;
    let re = Regex::new(&format!(r#"(?i)href="[^"]*MMSDM_{year}_(\d{{2}})/""#))?;
    let mut months: Vec<u32> = re
        .captures_iter(&html)
        .filter_map(|c| c.get(1).and_then(|m| m.as_str().parse().ok()))
        .collect();
    months.sort();
    months.dedup();
    Ok(months)
}
