/*
    Hurry Curry! - a game about cooking
    Copyright (C) 2026 Hurry Curry! Contributors

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, version 3 of the License only.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

*/
use anyhow::Result;
use clap::Parser;
use http_body_util::Full;
use hurrycurry_protocol::registry::Entry;
use hyper::{
    Response, StatusCode, body::Bytes, header::HeaderValue, server::conn::http1,
    service::service_fn,
};
use hyper_util::rt::TokioIo;
use log::warn;
use mdns_sd::{ServiceDaemon, ServiceEvent};
use std::{cmp::Reverse, collections::HashMap, net::SocketAddr, sync::Arc};
use tokio::{net::TcpListener, spawn, sync::RwLock};

#[derive(Parser)]
struct Args {
    /// Print version and exit
    #[arg(short, long)]
    version: bool,
}

fn main() -> anyhow::Result<()> {
    let args = Args::parse();
    if args.version {
        println!("{}", env!("CARGO_PKG_VERSION"));
        return Ok(());
    }

    env_logger::init_from_env("LOG");
    tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()?
        .block_on(async_main())?;
    Ok(())
}

async fn async_main() -> Result<anyhow::Error /*TODO: never type*/> {
    let mdns = ServiceDaemon::new()?;
    let mdns_events = mdns.browse("_hurrycurry._tcp.local.")?;

    let entries = Arc::new(RwLock::new(HashMap::<String, Entry>::new()));
    let entries2 = entries.clone();

    spawn(async move {
        while let Ok(event) = mdns_events.recv_async().await {
            match event {
                ServiceEvent::ServiceResolved(service_info) => {
                    entries2.write().await.insert(
                        service_info.get_fullname().to_owned(),
                        Entry {
                            name: service_info
                                .get_fullname()
                                .strip_suffix("._hurrycurry._tcp.local.")
                                .unwrap_or(service_info.get_fullname())
                                .to_owned(),
                            address: service_info
                                .get_addresses()
                                .iter()
                                .map(|a| {
                                    format!(
                                        "ws://{}",
                                        SocketAddr::new(a.to_ip_addr(), service_info.get_port()) //? Is is ok to drop ip6 scope?
                                    )
                                })
                                .collect(),
                            players_online: service_info
                                .get_property_val_str("players")
                                .and_then(|p| p.parse().ok())
                                .unwrap_or(0),
                            last_game: 0,
                            version: service_info
                                .get_property_val_str("protocol")
                                .and_then(|v| {
                                    let (maj, min) = v.split_once(".")?;
                                    Some((maj.parse().ok()?, min.parse().ok()?))
                                })
                                .unwrap_or((0, 0)),
                        },
                    );
                }
                ServiceEvent::ServiceRemoved(_, fullname) => {
                    entries2.write().await.remove(&fullname);
                }
                _ => (),
            }
        }
    });

    let listener = TcpListener::bind("127.0.0.1:27033").await?;
    loop {
        let (stream, _) = listener.accept().await?;
        let entries = entries.clone();
        spawn(async move {
            if let Err(e) = http1::Builder::new()
                .serve_connection(
                    TokioIo::new(stream),
                    service_fn(move |_req| {
                        let entries = entries.clone();
                        async move {
                            anyhow::Ok(match _req.uri().path() {
                                "/" => Response::new(Full::new(Bytes::from(
                                    "Hurry Curry! local discovery service",
                                ))),
                                "/v1/list" => {
                                    let mut ents = entries
                                        .read()
                                        .await
                                        .clone()
                                        .into_values()
                                        .collect::<Vec<_>>();
                                    ents.sort_by_key(|e| Reverse(e.players_online));

                                    let mut res = Response::new(Full::new(Bytes::from(
                                        serde_json::to_string(&ents).unwrap(),
                                    )));
                                    *res.status_mut() = StatusCode::OK;
                                    res.headers_mut().insert(
                                        "content-type",
                                        HeaderValue::from_static("application/json"),
                                    );
                                    res
                                }
                                _ => {
                                    let mut res =
                                        Response::new(Full::new(Bytes::from("not found")));
                                    *res.status_mut() = StatusCode::NOT_FOUND;
                                    res
                                }
                            })
                        }
                    }),
                )
                .await
            {
                warn!("conn failed: {e}");
            }
        });
    }
}
