package hsr.dsa.network;

import java.io.Closeable;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
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.BlockChainClient;
import hsr.dsa.blockchain.exceptions.SmartContractException;
import hsr.dsa.data.DataCallback;
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.network.data.ChatMessage;
import hsr.dsa.network.data.DHTGroup;
import hsr.dsa.network.data.FriendRequest;
import hsr.dsa.network.data.FriendRequest.RequestState;
import hsr.dsa.network.data.GroupInvite;
import hsr.dsa.network.data.NotaryMessage;
import hsr.dsa.network.data.PublicProfile;
import hsr.dsa.utils.ObservableList;
import hsr.dsa.utils.RSA;
import net.tomp2p.peers.Number160;

public class ChatClient implements NetworkEventHandler, Closeable {
	private static final Logger log = LoggerFactory.getLogger(ChatClient.class);
	private static final Random RANDOM = new Random();
	private final DataClient data;
	private final BlockChainClient blockChainClient;
	private final ScheduledExecutorService service;

	private final LocalUser user;
	private final List<DHTGroup> registeredGroups;
	
	public ChatClient(DataClient data, LocalUser user, BlockChainClient blockChainClient) {
		this.data = data;
		this.blockChainClient = blockChainClient;
		
		this.user = user;
		this.registeredGroups = new ArrayList<DHTGroup>();
		this.getRegisteredGroups();
		
		this.service = Executors.newSingleThreadScheduledExecutor();
		this.service.scheduleWithFixedDelay(new NetworkListener(data, this), 0, 1, TimeUnit.SECONDS);
		
	}

	@Override
	public Number160 getListenAddress() {
		return this.user.getId();
	}

	@Override
	public synchronized List<DHTGroup> getRegisteredGroups() {
		if(this.registeredGroups.size() != this.user.getGroups().size()) {
			Iterator<Group> iterator = new ArrayList<Group>(this.user.getGroups()).iterator();
			while(iterator.hasNext()) {
				Group group = iterator.next();
				boolean exists = false;
				for(DHTGroup registeredGroup : this.registeredGroups) {
					if(group.getId() == registeredGroup.getId()) {
						exists = true;
						break;
					}
				}
				if(!exists) {
					try {
						this.data.get(Number160.createHash(group.getId()), DHTGroup.class, (dhtGroup) -> {
							if(dhtGroup != null) {
								this.syncGroup(group, dhtGroup);
								synchronized (this.registeredGroups) {
									this.registeredGroups.add(dhtGroup);
									this.syncGroup(group, dhtGroup);
								}
							} else {
								log.debug("Group " + group.getId() + " not found");
								try {
									this.user.getGroups().remove(group);
								} catch (IllegalStateException e) {
									log.warn("Error removeing Groups", e);
								}
							}
						});
					} catch (ClassNotFoundException | IOException e) {
						log.error("Error loading Groups", e);
					}
				}
			}
		}
		return this.registeredGroups;
	}

	public void createFriendRequest(String friendName) throws IOException {
		Number160 hash = Number160.createHash(friendName);
		this.data.add(hash, new FriendRequest(this.user.getId(), hash, RequestState.open));
	}
	
	public void acceptFriendRequest(hsr.dsa.data.models.FriendRequest friendRequest) throws IOException {
		this.data.add(friendRequest.getRequest().getRequester(), new FriendRequest(friendRequest.getRequest().getRequester(), friendRequest.getRequest().getReplyer(), RequestState.accepted));
	}
	
	public void rejectFriendRequest(hsr.dsa.data.models.FriendRequest friendRequest) throws IOException {
		this.data.add(friendRequest.getRequest().getRequester(), new FriendRequest(friendRequest.getRequest().getRequester(), friendRequest.getRequest().getReplyer(), RequestState.rejected));
	}
	
	public void createGroup(String name, DataCallback<Group> callback) throws ClassNotFoundException, IOException {
		long groupId = RANDOM.nextLong();
		Number160 groupAddress = Number160.createHash(groupId);
		this.data.get(groupAddress, DHTGroup.class, (existingGroup) -> {
			if(existingGroup == null) {
				Set<Number160> members = new HashSet<Number160>();
				members.add(this.user.getId());
				DHTGroup group = new DHTGroup(this.user.getId(), groupId, name, members);
				try {
					this.data.put(groupAddress, group, (success) -> {
						synchronized (this.registeredGroups) {
							this.registeredGroups.add(group);
						}
					});
					ObservableList<User> groupMembers = new ObservableList<User>();
					groupMembers.add(this.user);
					callback.onData(new Group(groupId, group.getName(), groupMembers));
				} catch (IOException e) {
					e.printStackTrace();
					callback.onData(null);
				}
			} else {
				callback.onData(null);
			}
		});
	}
	
	private void getGroupById(Number160 group, DataCallback<DHTGroup> callback) throws ClassNotFoundException, IOException {
		this.data.get(group, DHTGroup.class, callback);
	}
	
	public void getGroupById(long groupId, DataCallback<DHTGroup> callback) throws ClassNotFoundException, IOException {
		this.getGroupById(Number160.createHash(groupId), callback);
	}

	public void syncGroup(Group group, DHTGroup dhtGroup) {
		List<User> members = group.getMembers();
		for(Number160 id : dhtGroup.getMembers()) {
			User user = null;
			for(User member : members) {
				if(member.getId().equals(id)) {
					user = member;
					break;
				}
			}
			if(user == null) {
				try {
					this.data.getProfile(id, (profile)->{
						synchronized (members) {
							try {
								members.add(new RemoteUser(id, profile.getUsername(), new ObservableList<>(), new RSA(profile.getPublicKey()), profile.getBlockChainPublicKey()));
							} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
								e.printStackTrace();
							}
						}
					});
				} catch (ClassNotFoundException | IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}
	
	public void joinGroup(long groupId, String password) throws ClassNotFoundException, IOException {
		Number160 groupAddress = Number160.createHash(groupId);
		this.data.get(groupAddress, DHTGroup.class, (group) -> {
			if(group != null) {
				group.getMembers().add(this.user.getId());
				synchronized (this.registeredGroups) {
					this.registeredGroups.add(group);
				}
				try {
					this.data.put(groupAddress, group, (success) -> {});
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		});
	}
	
	public void inviteToGroup(String user, Group group) throws IOException {
		Number160 userId = Number160.createHash(user);
		Number160 groupId = Number160.createHash(group.getId());
		this.data.add(userId, new GroupInvite(this.user.getId(), groupId, ""));
	}
	
	public void writeDirectMessage(User user, RemoteUser friend, String message) throws IOException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
		this.data.add(friend.getId(), new ChatMessage(System.currentTimeMillis(), user.getId(), message));
	}
	
	public void writeNotaryMessage(User user, RemoteUser friend, String message) throws NoSuchAlgorithmException, IOException, SmartContractException {
		NotaryMessage notaryMessage = new NotaryMessage(System.currentTimeMillis(), user.getId(), message);
		this.blockChainClient.upload(notaryMessage.getHash(), friend.getBlockChainPublicKey());
		this.data.add(friend.getId(), notaryMessage);
	}
	
	public void writeGroupMessage(User user, Group group, String message) throws IOException {
		this.data.add(Number160.createHash(group.getId()), new ChatMessage(System.currentTimeMillis(), user.getId(), message));
	}

	@Override
	public void onFriendRequest(FriendRequest friendRequest) {
		try {
			if(friendRequest.getState() == RequestState.open) {
				this.data.getProfile(friendRequest.getRequester(), (PublicProfile requester) -> {
					try {
						RemoteUser user = new RemoteUser(friendRequest.getRequester(), requester.getUsername(), new ObservableList<>(), new RSA(requester.getPublicKey()), requester.getBlockChainPublicKey());
						this.user.getFriendRequests().add(new hsr.dsa.data.models.FriendRequest(user, friendRequest));
					} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
						log.error("ERROR could not encrypt friendRequest", e);
					}
				});
			} else if(friendRequest.getState() == RequestState.accepted) {
				this.data.getProfile(friendRequest.getReplyer(), (PublicProfile replyer) -> {
					try {
						RemoteUser user = new RemoteUser(friendRequest.getReplyer(), replyer.getUsername(), new ObservableList<>(), new RSA(replyer.getPublicKey()), replyer.getBlockChainPublicKey());
						List<User> friends = this.user.getFriends();
						if(!friends.contains(user)) {
							friends.add(user);
						}
					} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
						log.error("ERROR could not encrypt friendRequest", e);
					}
				});
			} else {
				//TODO rejected notification
			}
		} catch (ClassNotFoundException | IOException e) {
			log.error("ERROR could not parse friendRequest", e);
		}
	}

	@Override
	public void onGroupInvite(GroupInvite groupInvite) {
		try {
			Semaphore lock = new Semaphore(0);
			
			AtomicReference<RemoteUser> userRef = new AtomicReference<RemoteUser>();
			this.data.getProfile(groupInvite.getSender(), (sender) -> {
				try {
					if(sender != null) {
						userRef.set(new RemoteUser(sender.getSender(), sender.getUsername(), new ObservableList<>(), new RSA(sender.getPublicKey()), sender.getBlockChainPublicKey()));
					}
				} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
					log.error("ERROR could not parse GroupInvite", e);
				} finally {
					lock.release();
				}
			});
			
			AtomicReference<Group> group = new AtomicReference<Group>();
			this.getGroupById(groupInvite.getGroup(), (dhtGroup) -> {
				try {
					if(dhtGroup != null) {
						group.set(new Group(dhtGroup.getId(), dhtGroup.getName(), new ObservableList<>()));
					}
				} finally {
					lock.release();
				}
			});
			
			(new Thread() {
				public void run() {
					try {
						lock.acquire(2);
						if(userRef.get() != null && group.get() != null) {
							user.getGroupInvite().add(new hsr.dsa.data.models.GroupInvite(userRef.get(), group.get()));
						} else {
							log.error("ERROR could not parse GroupInvite");
						}
					} catch (InterruptedException e) {
						log.error("ERROR could not parse GroupInvite", e);
					}
				}
			}).start();
		} catch (IOException | ClassNotFoundException e) {
			log.error("ERROR could not parse GroupInvite", e);
		}
	}

	@Override
	public void onDirectMessage(ChatMessage message) {
		for (User user : this.user.getFriends()) {
			if(user.getId().equals(message.getCreator()) && user instanceof RemoteUser) {
				if(message instanceof NotaryMessage) {
					NotaryMessage notaryMessage = (NotaryMessage)message;
					((RemoteUser)user).getChatMessages().add(new hsr.dsa.data.models.NotaryMessage(user, Instant.ofEpochMilli(notaryMessage.getCreationTime()).atZone(ZoneId.systemDefault()).toLocalDateTime(), notaryMessage.getMessage(), notaryMessage.getHash()));
					(new Thread() {
						public void run() {
							try {
								while(blockChainClient.verifyUpload(notaryMessage.getHash(), user.getBlockChainPublicKey()) == 0) {
									try {
										Thread.sleep(10);
									} catch (InterruptedException e) {
										log.warn("interrupded", e);
									}
								}
								blockChainClient.received(notaryMessage.getHash());
								
							} catch (SmartContractException e) {
								log.error("Unable to confirm reception of notary message", e);
							}
						}
					}).start();
				} else {
					((RemoteUser)user).getChatMessages().add(new TextMessage(user, Instant.ofEpochMilli(message.getCreationTime()).atZone(ZoneId.systemDefault()).toLocalDateTime(), message.getMessage()));
				}
				break;
			}
		}
	}

	@Override
	public void onGroupMessage(ChatMessage message, DHTGroup dhtGroup) {
		for (Group group : this.user.getGroups()) {
			if(group.getId() == dhtGroup.getId()) {
				User user = null;
				if(message.getSender().equals(this.user.getId())) {
					user = this.user;
				} else {
					for(User member : group.getMembers()) {
						if(message.getSender().equals(member.getId())) {
							user = member;
							break;
						}
					}
				}
				if(user == null) {
					try {
						this.data.getProfile(message.getCreator(), (creator) -> {
							if(creator != null) {
								User remoteUser = null;
								try {
									remoteUser = new RemoteUser(creator.getSender(), creator.getUsername(), new ObservableList<>(), new RSA(creator.getPublicKey()), creator.getBlockChainPublicKey());
									group.getMembers().add(remoteUser);
								} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
									log.error("could not find creator of message", e);
								}
								onGroupMessage(group, message, remoteUser);
							} else {
								log.error("could not find creator of message");
								onGroupMessage(group, message, null);
							}
						});
					} catch (ClassNotFoundException | IOException e) {
						log.error("could not find creator of message", e);
						onGroupMessage(group, message, null);
					}
				} else {
					onGroupMessage(group, message, user);
				}
				break;
			}
		}
	}
	
	private void onGroupMessage(Group group, ChatMessage message, User user) {
		TextMessage textMessage = new TextMessage(user, Instant.ofEpochMilli(message.getCreationTime()).atZone(ZoneId.systemDefault()).toLocalDateTime(), message.getMessage());
		if(!group.getChatMessages().contains(textMessage)) {
			group.getChatMessages().add(textMessage);
		}
	}

	@Override
	public void close() throws IOException {
		this.service.shutdown();
	}

	public void acceptGroupInvitation(hsr.dsa.data.models.GroupInvite groupInvite) {
		this.user.getGroups().add(groupInvite.getGroup());
		try {
			this.joinGroup(groupInvite.getGroup().getId(), "");
		} catch (ClassNotFoundException | IOException e) {
			log.error("Error Joining group", e);
		}
	}

	@Override
	public List<AcitveProfile> getFriendList() {
		List<AcitveProfile> acitveProfiles = new ArrayList<AcitveProfile>();
		if(this.user != null && this.user.getFriends() != null) {
			acitveProfiles.addAll(this.user.getFriends());
		}
		return acitveProfiles;
	}
}
