/*
 * Copyright 2020 Skyscanner Limited.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

use crate::config::Dependency;
use crate::config::ProtovendConfig;
use crate::git;
use crate::git_url::GitUrl;
use crate::util;
use crate::{date_compat, Result};
use chrono::{Local, NaiveDateTime};
use failure::format_err;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::path::Path;

pub mod vendor;

#[cfg(test)]
#[path = "../tests_utils/mod.rs"]
mod tests_utils;

pub const PROTOVEND_LOCK: &str = ".protovend.lock";

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
struct Import {
    branch: String,
    commit: String,
    url: GitUrl,
    proto_dir: String,
    proto_paths: Vec<String>,
    filename_regex: String,
    resolve_dependency: bool,
}

#[derive(Deserialize)]
struct LegacyImport {
    branch: String,
    commit: String,
    url: GitUrl,
    proto_dir: String,
    proto_paths: Vec<String>,
}

impl From<LegacyImport> for Import {
    fn from(import: LegacyImport) -> Self {
        Import {
            url: import.url.clone(),
            branch: import.branch,
            commit: import.commit,
            proto_dir: import.proto_dir,
            proto_paths: import.proto_paths,
            filename_regex: String::from(".*"),
            resolve_dependency: false,
        }
    }
}

impl PartialEq<Dependency> for Import {
    fn eq(&self, other: &Dependency) -> bool {
        self.url == other.url && self.branch == other.branch
    }
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct ProtovendLock {
    imports: Vec<Import>,
    min_protovend_version: Version,
    #[serde(with = "date_compat")]
    updated: NaiveDateTime,
}

#[derive(Deserialize)]
pub struct LegacyProtovendLock {
    imports: Vec<LegacyImport>,
    min_protovend_version: Version,
    #[serde(with = "date_compat")]
    updated: NaiveDateTime,
}

impl From<LegacyProtovendLock> for ProtovendLock {
    fn from(legacy_config: LegacyProtovendLock) -> Self {
        ProtovendLock {
            min_protovend_version: legacy_config.min_protovend_version,
            imports: legacy_config
                .imports
                .into_iter()
                .map(|d| d.into())
                .collect(),
            updated: legacy_config.updated,
        }
    }
}

#[derive(Deserialize)]
#[serde(untagged)]
enum Lock {
    Lock(ProtovendLock),
    Legacy(LegacyProtovendLock),
}

impl ProtovendLock {
    pub fn vendor(&self) -> Result<()> {
        vendor::prepare_output_directory()?;

        for import in self.imports.iter() {
            vendor::vendor_import(import)?;
        }

        Ok(())
    }

    fn write(&mut self) -> Result<()> {
        let f = File::create(Path::new(PROTOVEND_LOCK))?;
        self.imports.sort_by(|a, b| a.url.cmp(&b.url));
        self.updated = Local::now().naive_local();
        Ok(serde_yaml::to_writer(f, &self)?)
    }

    fn process_new_imports(&self, deps: Vec<Dependency>) -> Result<Vec<Import>> {
        let (mut entries, added_entries) = diff_lock(deps, self.imports.clone());

        let new_entries: Result<Vec<Import>> = added_entries.into_iter().map(to_import).collect();
        entries.append(&mut new_entries?);

        Ok(entries)
    }

    pub fn update_imports(&mut self, config: ProtovendConfig) -> Result<()> {
        let new_imports = self.process_new_imports(config.vendor)?;
        if new_imports != self.imports {
            self.imports = new_imports;
            self.write()
        } else {
            Ok(())
        }
    }

    pub fn clear_all_imports(&mut self) {
        self.imports.clear()
    }

    pub fn clear_imports(&mut self, repo: GitUrl) {
        self.imports.retain(|import| import.url != repo)
    }
}

pub fn load_lock() -> Result<ProtovendLock> {
    load_lockfile(Path::new(PROTOVEND_LOCK))
}

fn load_lockfile(lock_file: &Path) -> Result<ProtovendLock> {
    if lock_file.exists() {
        let f = File::open(lock_file)?;
        let lock: Lock = serde_yaml::from_reader(f)?;

        let lock: ProtovendLock = match lock {
            Lock::Lock(p) => p,
            Lock::Legacy(l) => l.into(),
        };

        if util::is_valid_version(&lock.min_protovend_version) {
            Ok(lock)
        } else {
            Err(format_err!("protovend cli version {} is too old for included metadata files. Minimum version must be {}", *crate::CRATE_VERSION, lock.min_protovend_version))
        }
    } else {
        Ok(ProtovendLock {
            imports: Vec::new(),
            min_protovend_version: crate::CRATE_VERSION.clone(),
            updated: Local::now().naive_local(),
        })
    }
}

fn to_import(dep: Dependency) -> Result<Import> {
    Ok(Import {
        commit: git::get_latest_commit_sha(&dep.url, &dep.branch)?.to_string(),
        branch: dep.branch,
        url: dep.url,
        proto_dir: dep.proto_dir,
        proto_paths: dep.proto_paths,
        filename_regex: dep.filename_regex,
        resolve_dependency: dep.resolve_dependency,
    })
}

fn diff_lock(
    mut deps: Vec<Dependency>,
    mut imports: Vec<Import>,
) -> (Vec<Import>, Vec<Dependency>) {
    let mut retained_imports = Vec::new();
    deps.retain(|dep| {
        if let Some(position) = imports.iter().position(|import| import == dep) {
            retained_imports.push(imports.remove(position));
            false
        } else {
            true
        }
    });

    (retained_imports, deps)
}

pub fn init() -> Result<()> {
    let protovend_lock_path = Path::new(PROTOVEND_LOCK);
    if protovend_lock_path.exists() {
        log::warn!("{} file already exists in project", PROTOVEND_LOCK);
        Ok(())
    } else {
        let mut lock = ProtovendLock {
            imports: Vec::new(),
            min_protovend_version: crate::CRATE_VERSION.clone(),
            updated: Local::now().naive_local(),
        };
        lock.write()
            .map(|_| log::info!("Created {}", PROTOVEND_LOCK))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_correctly_parses_lock() {
        let lock_contents = "--- \
             \nimports: \
             \n  - branch: master \
             \n    commit: a9fef901ae63f689a4180bf8255d16a45baf04a1 \
             \n    url: git@github.skyscannertools.net:cell-placement/cell-metadata-service.git \
             \n    proto_dir: proto \
             \n    proto_paths: \
             \n      - path/to \
             \n    filename_regex: \".*\"\
             \n    resolve_dependency: true \
             \nmin_protovend_version: 0.1.8 \
             \nupdated: \"2019-11-20 15:02:12.330896\"";

        let lock_path = tests_utils::fs::write_contents_to_temp_file(lock_contents, "lock");

        let expected_lock = ProtovendLock {
            imports: vec![Import {
                branch: String::from("master"),
                commit: String::from("a9fef901ae63f689a4180bf8255d16a45baf04a1"),
                url: "git@github.skyscannertools.net:cell-placement/cell-metadata-service.git"
                    .parse::<GitUrl>()
                    .unwrap(),
                proto_dir: String::from("proto"),
                proto_paths: vec![String::from("path/to")],
                filename_regex: String::from(".*"),
                resolve_dependency: true,
            }],
            min_protovend_version: "0.1.8".parse::<Version>().unwrap(),
            updated: "2019-11-20T15:02:12.330896"
                .parse::<NaiveDateTime>()
                .unwrap(),
        };

        let actual_lock = load_lockfile(&lock_path).unwrap();

        assert_eq!(expected_lock, actual_lock);
    }
}
