package hsr.dsa.data;

import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import hsr.dsa.blockchain.BlockChainUtils;
import hsr.dsa.data.models.ChatMessage;
import hsr.dsa.data.models.Group;
import hsr.dsa.data.models.LocalUser;
import hsr.dsa.data.models.RemoteUser;
import hsr.dsa.data.models.TextMessage;
import hsr.dsa.data.models.User;
import hsr.dsa.local.ChatHistory;
import hsr.dsa.local.Friend;
import hsr.dsa.local.LocalData;
import hsr.dsa.local.InvalidDataException;
import hsr.dsa.local.Profile;
import hsr.dsa.network.DataClient;
import hsr.dsa.network.NetworkManager;
import hsr.dsa.network.data.PublicProfile;
import hsr.dsa.network.exceptions.DublicateEntryException;
import hsr.dsa.utils.ObservableList;
import hsr.dsa.utils.RSA;
import net.tomp2p.peers.Number160;

public class DataManager {
	private static final Logger log = LoggerFactory.getLogger(DataManager.class);
	private final Executor executor;
	private final Map<String, PublicProfile> contacts;

	private final LocalData localData;
	private DataClient dataClient;

	public DataManager() throws IOException {
		this.executor = Executors.newCachedThreadPool();
		this.contacts = new HashMap<String, PublicProfile>();
		this.localData = new LocalData();
	}
	
	public List<String> getProfiles() {
		try {
			return this.localData.getProfileList();
		} catch (InvalidDataException e) {
			throw new IllegalStateException("Could not get profile list");
		}
	}

	public LocalUser getProfile(String username, String password) throws Exception {
		this.init();
		return this.convertToUser(this.localData.loadProfileData(username, password), password);
	}

	public LocalUser createProfile(String username, String password, String blockChainPrivateKey) throws InvalidDataException, Exception {
		this.init();
		String blockChainPublicKey = BlockChainUtils.fromECPrivateKey(blockChainPrivateKey).getAddress();
		final Semaphore lock = new Semaphore(0);
		AtomicReference<Boolean> succsesRef = new AtomicReference<Boolean>();
		Profile profile = this.localData.createProfileData(username, password, blockChainPrivateKey);
		this.dataClient.createProfile(new PublicProfile(username, profile.getRsa().getPublicKey(), blockChainPublicKey), succses -> {
			succsesRef.set(succses);
			lock.release();
		});
		lock.acquire();
		if(succsesRef.get()) {
			return this.convertToUser(profile, password);
		} else {
			this.localData.deleteProfileData(profile.getUsername());
			throw new IllegalStateException("Could not create profile");
		}
	}
	
	private void republishProfile(LocalUser profile, String password) throws ClassNotFoundException, DublicateEntryException, IOException, InterruptedException {
		this.init();
		final Semaphore lock = new Semaphore(0);
		AtomicReference<Boolean> succsesRef = new AtomicReference<Boolean>();
		this.dataClient.createProfile(new PublicProfile(profile.getName(), profile.getRas().getPublicKey(), profile.getBlockChainPublicKey()), succses -> {
			succsesRef.set(succses);
			lock.release();
		});
		lock.acquire();
		if(!succsesRef.get()) {
			throw new IllegalStateException("Could not create profile");
		}
	}

	public void login(LocalUser profile, String password) {
		this.init();
		try {
			final Semaphore lock = new Semaphore(0);
			AtomicReference<PublicProfile> publicProfileRef = new AtomicReference<PublicProfile>();
			this.dataClient.getProfile(profile.getName(), (publicProfile) -> {
				publicProfileRef.set(publicProfile);
				lock.release();
			});
			lock.acquire();
			if(publicProfileRef.get() == null) {
				this.republishProfile(profile, password);
			}
		} catch (Exception e) {
			throw new IllegalStateException("Error trying to login", e); //TODO better exception
		}
	}

	public void logout() throws DataManagerException {
		try {
			this.localData.closeProfiles();
		} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException | InvalidAlgorithmParameterException | IOException | InvalidDataException e) {
			throw new DataManagerException("Could not get encrypt and save profiles", e);
		}
	}

	private LocalUser convertToUser(Profile profile, String password) throws NoSuchAlgorithmException, NoSuchPaddingException, ClassNotFoundException, IOException {
		if(profile == null) {
			throw new IllegalStateException("Selectes User doesnt Exist");
		}
		List<User> friends = new ArrayList<User>();
		List<Group> groups = new ArrayList<Group>();
		
		LocalUser user = new LocalUser(Number160.createHash(profile.getUsername()), profile.getUsername(), new ObservableList<User>(friends), new ObservableList<Group>(groups), profile.getRsa(), password, profile.getBlockChainPrivateKey(), BlockChainUtils.fromECPrivateKey(profile.getBlockChainPrivateKey()).getAddress());
		if(profile.getFriends() != null) {
			for(Friend friend : profile.getFriends()) {
				List<ChatMessage> history = new ArrayList<ChatMessage>();
				RSA rsa = new RSA();
				Number160 friendId = Number160.createHash(friend.getFriendName());
				RemoteUser friendUser = new RemoteUser(friendId, friend.getFriendName(), new ObservableList<ChatMessage>(history), rsa, friend.getBlockChainAddress());
				
				this.dataClient.getProfile(friendId, (PublicProfile friendProfile) -> {
					if(friendProfile != null) {
						rsa.setPublicKey(friendProfile.getPublicKey());
					}
				});
				
				friends.add(friendUser);
				
				if(friend.getChatHistory() != null && friend.getChatHistory().getAllMessageHistory() != null) {
					for(Entry<LocalDateTime, hsr.dsa.local.ChatMessage> chatMessage : friend.getChatHistory().getAllMessageHistory().entrySet()) {
						if(chatMessage.getValue().getUsername().equals(user.getName())) {
							history.add(new TextMessage(user, chatMessage.getKey(), chatMessage.getValue().getMessage()));
						} else {
							history.add(new TextMessage(friendUser, chatMessage.getKey(), chatMessage.getValue().getMessage()));
						}
					}
				}
			}
		}
		
		for(hsr.dsa.local.Group group : profile.getGroups()) {
			groups.add(new Group(group.getGroupId(), group.getName(), new ObservableList<User>()));
		}
		return user;
	}

	private void init() {
		if(this.dataClient == null) {
			this.dataClient = NetworkManager.getDataClient();
		}
	}

	public boolean removeProfile(String username) throws IllegalArgumentException {
		return false;
	}

	public Profile loginProfile() throws IllegalArgumentException {
		return null;
	}
	
	public PublicProfile getUser(String username) {
		PublicProfile contact = this.contacts.get(username);
		if(contact != null) {
			return contact;
		} else {
			final Semaphore lock = new Semaphore(0);
			final AtomicReference<PublicProfile> ref = new AtomicReference<PublicProfile>();
			executor.execute(() -> {
				try {
					dataClient.getProfile(username, (profile) -> {
						ref.set(profile);
					});
				} catch (ClassNotFoundException | IOException e) {
					log.warn("unable to get values", e);
				} finally {
					lock.release();
				}
			});
			try {
				lock.acquire();
				this.contacts.put(username, ref.get());
				return ref.get();
			} catch (InterruptedException e) {
				log.warn("Interrupded while waitung for values", e);
				return null;
			}
			
		}
	}
	
	public List<PublicProfile> getFriends(Profile profile) {
		List<PublicProfile> friends = new ArrayList<PublicProfile>();
		Map<Semaphore, AtomicReference<PublicProfile>> friendRequests = new HashMap<Semaphore, AtomicReference<PublicProfile>>();
		for(Friend friend : profile.getFriends()) {
			PublicProfile friendProfile = contacts.get(friend.getFriendName());
			if(friendProfile != null) {
				friends.add(friendProfile);
			} else {
				final Semaphore lock = new Semaphore(0);
				final AtomicReference<PublicProfile> ref = new AtomicReference<PublicProfile>();
				friendRequests.put(lock, ref);
				executor.execute(() -> {
					try {
						dataClient.getProfile(friend.getFriendName(), (publicProfile) -> {
							ref.set(publicProfile);
						});
					} catch (ClassNotFoundException | IOException e) {
						log.warn("unable to get values", e);
					} finally {
						lock.release();
					}
				});
			}
		}
		for(Entry<Semaphore, AtomicReference<PublicProfile>> friendRequest : friendRequests.entrySet()) {
			try {
				friendRequest.getKey().acquire();
				PublicProfile publicProfile = friendRequest.getValue().get();
				this.contacts.put(publicProfile.getUsername(), publicProfile);
				friends.add(publicProfile);
			} catch (InterruptedException e) {
				log.warn("Interrupded while waitung for values", e);
			}
		}
		return friends;
	}

	public void stop() {
		try {
			this.localData.closeProfiles();
		} catch (Exception e) {
			log.error("error shuting down", e);
		}
		try {
			if(this.dataClient != null) {
				this.dataClient.close();
			}
		} catch (IOException e) {
			log.error("error shuting down", e);
		}
		System.exit(0);
	}

	public DataClient getNetworkDataClient() {
		return this.dataClient;
	}

	public synchronized void saveProfile(LocalUser user) {
		try {
			if(this.localData != null && user != null) {
				Profile profile = this.localData.loadProfileData(user.getName(), user.getPassword());
				List<Friend> friends = profile.getFriends();
				for(User remoteUser : user.getFriends()) {
					boolean existingFriend = false;
					for(Friend friend : friends) {
						if(friend.getFriendName().equals(remoteUser.getName())) {
							this.updateFriend(friend, (RemoteUser)remoteUser);
							existingFriend = true;
							break;
						}
					}
					if(!existingFriend) {
						friends.add(new Friend(remoteUser.getName(), remoteUser.getBlockChainPublicKey()));
					}
				}
				for(Group group : user.getGroups()) {
					hsr.dsa.local.Group g = new hsr.dsa.local.Group(group.getId(), group.getName());
					if(!profile.getGroups().contains(g)) {
						profile.addGroup(g);
					}
				}
				this.localData.saveProfileData(profile);
			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private void updateFriend(Friend friend, RemoteUser remoteUser) {
		ChatHistory chatHistory = friend.getChatHistory();
		if(chatHistory == null) {
			chatHistory = new ChatHistory();
			friend.setChatHistory(chatHistory);
		}
		if(chatHistory.getAllMessageHistory() == null) {
			chatHistory.setMessageHistory(new TreeMap<>());
		}
		TreeMap<LocalDateTime, hsr.dsa.local.ChatMessage> history = chatHistory.getAllMessageHistory();
		for(ChatMessage chatMessage : remoteUser.getChatMessages()) {
			if(chatMessage instanceof TextMessage) {
				history.put(chatMessage.getDate(), new hsr.dsa.local.ChatMessage(chatMessage.getSender().getName(), ((TextMessage)chatMessage).getMessage()));
			}
		}
	}
}