// Copyright (c) 2021  Teddy Wing
//
// This file is part of Reflectub.
//
// Reflectub is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Reflectub 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Reflectub. If not, see .
use sqlx::{self, ConnectOptions, Connection, Executor, Row};
use thiserror;
use crate::github;
/// Repository metadata mapped to the database.
#[derive(Debug)]
pub struct Repo {
    id: i64,
    name: Option,
    description: Option,
    updated_at: Option,
}
impl Repo {
    pub fn description(&self) -> &str {
        self.description
            .as_deref()
            .unwrap_or("")
    }
}
impl From<&github::Repo> for Repo {
    fn from(repo: &github::Repo) -> Self {
        Self {
            id: repo.id,
            name: Some(repo.name.clone()),
            description: repo.description.clone(),
            updated_at: Some(repo.updated_at.clone()),
        }
    }
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("database error")]
    Db(#[from] sqlx::Error),
}
#[derive(Debug)]
pub struct Db {
    connection: sqlx::SqliteConnection,
}
impl Db {
    /// Open a connection to the database.
    pub async fn connect(path: &str) -> Result {
        Ok(
            Db {
                connection: sqlx::sqlite::SqliteConnectOptions::new()
                    .filename(path)
                    .create_if_missing(true)
                    .connect()
                    .await?,
            }
        )
    }
    /// Initialise the database with tables and indexes.
    pub async fn create(&mut self) -> Result<(), Error> {
        let mut tx = self.connection.begin().await?;
        tx.execute(r#"
            CREATE TABLE IF NOT EXISTS repositories (
                id INTEGER PRIMARY KEY,
                name TEXT NOT NULL,
                description TEXT,
                updated_at TEXT NOT NULL
            );
        "#).await?;
        tx.execute(r#"
            CREATE UNIQUE INDEX IF NOT EXISTS idx_repositories_id
                ON repositories (id);
        "#).await?;
        tx.commit().await?;
        Ok(())
    }
    /// Get a repository by its ID.
    ///
    /// Returns a `sqlx::Error::RowNotFound` error if the row doesn't exist.
    pub async fn repo_get(&mut self, id: i64) -> Result {
        let mut tx = self.connection.begin().await?;
        let row = sqlx::query(r#"
            SELECT
                id,
                name,
                description,
                updated_at
            FROM repositories
            WHERE id = ?
        "#)
            .bind(id)
            .fetch_one(&mut tx)
            .await?;
        tx.commit().await?;
        Ok(
            Repo {
                id: row.get(0),
                name: Some(row.get(1)),
                description: row.get(2),
                updated_at: Some(row.get(3)),
            }
        )
    }
    /// Insert a new repository.
    pub async fn repo_insert(&mut self, repo: Repo) -> Result<(), Error> {
        let mut tx = self.connection.begin().await?;
        sqlx::query(r#"
            INSERT INTO repositories
                (id, name, description, updated_at)
                VALUES
                (?, ?, ?, ?)
        "#)
            .bind(repo.id)
            .bind(&repo.name)
            .bind(&repo.description)
            .bind(&repo.updated_at)
            .execute(&mut tx)
            .await?;
        tx.commit().await?;
        Ok(())
    }
    /// Check if the given repository is newer than the one in the repository.
    ///
    /// Compares the `updated_at` field to find out whether the repository was
    /// updated.
    pub async fn repo_is_updated(
        &mut self,
        repo: &Repo,
    ) -> Result {
        let mut tx = self.connection.begin().await?;
        let is_updated = match sqlx::query(r#"
            SELECT 1
            FROM repositories
            WHERE id = ?
                AND datetime(updated_at) < datetime(?)
        "#)
            .bind(repo.id)
            .bind(&repo.updated_at)
            .fetch_optional(&mut tx)
            .await
        {
            Ok(Some(_)) => Ok(true),
            Ok(None) => Ok(false),
            Err(e) => Err(e.into()),
        };
        tx.commit().await?;
        is_updated
    }
    /// Update an existing repository.
    pub async fn repo_update(&mut self, repo: &Repo) -> Result<(), Error> {
        let mut tx = self.connection.begin().await?;
        sqlx::query(r#"
            UPDATE repositories
            SET
                name = ?,
                description = ?,
                updated_at = ?
            WHERE id = ?
        "#)
            .bind(&repo.name)
            .bind(&repo.description)
            .bind(&repo.updated_at)
            .bind(repo.id)
            .execute(&mut tx)
            .await?;
        tx.commit().await?;
        Ok(())
    }
}