use crate::ServerError;
use ipnetwork::IpNetwork;
use rusqlite::{params, Connection};
use shared::{Cidr, CidrContents};
use std::ops::Deref;

pub static CREATE_TABLE_SQL: &str = "CREATE TABLE cidrs (
      id               INTEGER PRIMARY KEY,
      name             TEXT NOT NULL UNIQUE,
      ip               TEXT NOT NULL,
      prefix           INTEGER NOT NULL,
      parent           INTEGER REFERENCES cidrs,
      UNIQUE(ip, prefix),
      FOREIGN KEY (parent)
         REFERENCES cidrs (id)
            ON UPDATE RESTRICT
            ON DELETE RESTRICT
    )";

pub struct DatabaseCidr {
    inner: Cidr,
}

impl From<Cidr> for DatabaseCidr {
    fn from(inner: Cidr) -> Self {
        Self { inner }
    }
}

impl Deref for DatabaseCidr {
    type Target = Cidr;

    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl DatabaseCidr {
    pub fn create(conn: &Connection, contents: CidrContents) -> Result<Cidr, ServerError> {
        let CidrContents { name, cidr, parent } = &contents;

        log::debug!("creating {:?}", contents);

        let attached_peers = conn.query_row(
            "SELECT COUNT(*) FROM peers WHERE cidr_id = ?1",
            params![parent],
            |row| row.get::<_, u32>(0),
        )?;
        if attached_peers > 0 {
            log::warn!("tried to add a CIDR to a parent that has peers assigned to it.");
            return Err(ServerError::InvalidQuery);
        }

        if let Some(parent_id) = parent {
            let cidrs = Self::list(conn)?;

            let closest_parent = cidrs
                .iter()
                .filter(|current| cidr.is_subnet_of(current.cidr))
                .max_by_key(|current| current.cidr.prefix());

            if let Some(closest_parent) = closest_parent {
                if closest_parent.id != *parent_id {
                    log::warn!("tried to add a CIDR at the incorrect place in the tree (should be added to {}).", closest_parent.name);
                    return Err(ServerError::InvalidQuery);
                }
            } else {
                log::warn!("tried to add a CIDR outside of the root network range.");
                return Err(ServerError::InvalidQuery);
            }

            let parent_cidr = Self::get(conn, *parent_id)?.cidr;
            if !parent_cidr.contains(cidr.network()) || !parent_cidr.contains(cidr.broadcast()) {
                log::warn!("tried to add a CIDR with a network range outside of its parent.");
                return Err(ServerError::InvalidQuery);
            }
        }

        let overlapping_sibling = Self::list(conn)?
            .iter()
            .filter(|current| current.parent == *parent)
            .map(|sibling| sibling.cidr)
            .any(|sibling| {
                cidr.contains(sibling.network())
                    || cidr.contains(sibling.broadcast())
                    || sibling.contains(cidr.network())
                    || sibling.contains(cidr.broadcast())
            });

        if overlapping_sibling {
            log::warn!("tried to add a CIDR that overlaps with a sibling.");
            return Err(ServerError::InvalidQuery);
        }

        conn.execute(
            "INSERT INTO cidrs (name, ip, prefix, parent)
              VALUES (?1, ?2, ?3, ?4)",
            params![name, cidr.ip().to_string(), cidr.prefix() as i32, parent],
        )?;
        let id = conn.last_insert_rowid();
        Ok(Cidr { id, contents })
    }

    pub fn delete(conn: &Connection, id: i64) -> Result<(), ServerError> {
        conn.execute("DELETE FROM cidrs WHERE id = ?1", params![id])?;
        Ok(())
    }

    fn from_row(row: &rusqlite::Row) -> Result<Cidr, rusqlite::Error> {
        let id = row.get(0)?;
        let name = row.get(1)?;
        let ip: String = row.get(2)?;
        let prefix = row.get(3)?;
        let cidr = IpNetwork::new(
            ip.parse()
                .map_err(|_| rusqlite::Error::ExecuteReturnedResults)?,
            prefix,
        )
        .map_err(|_| rusqlite::Error::ExecuteReturnedResults)?;
        let parent = row.get(4)?;
        Ok(Cidr {
            id,
            contents: CidrContents { name, cidr, parent },
        })
    }

    pub fn get(conn: &Connection, id: i64) -> Result<Cidr, ServerError> {
        Ok(conn.query_row(
            "SELECT id, name, ip, prefix, parent FROM cidrs WHERE id = ?1",
            params![id],
            Self::from_row,
        )?)
    }

    pub fn list(conn: &Connection) -> Result<Vec<Cidr>, ServerError> {
        let mut stmt = conn.prepare_cached("SELECT id, name, ip, prefix, parent FROM cidrs")?;
        let cidr_iter = stmt.query_map(params![], Self::from_row)?;

        Ok(cidr_iter.collect::<Result<Vec<_>, rusqlite::Error>>()?)
    }
}
