import { Injectable } from '@angular/core';
import { Subject, BehaviorSubject, Observable } from 'rxjs';
import io from 'socket.io-client';
import * as _ from 'lodash';
import { ChatConversation } from 'app/chat/models/chat-conversation';
import { ChatUser } from 'app/chat/models/chat-user';
import { ChatMessage } from 'app/chat/models/chat-message';
import { ChatStatus } from 'app/chat/models/chat-status.enum';
import { NewMessage } from 'app/chat/models/server-responses/new-message';
import { Contacts } from 'app/chat/models/server-responses/contacts';
import { UserStatusUpdate } from 'app/chat/models/server-responses/user-status-update';
import { Conversation } from 'app/chat/models/server-responses/conversation';
import { HttpClient } from '@angular/common/http';
import { ChatMessageType } from 'app/chat/models/chat-message-type.enum';
import { concat } from 'rxjs/observable/concat';
import { UserService } from 'app/services/user.service';

import { environment } from '../../../environments/environment';

@Injectable()
export class ChatService {

  connection: any;

  // private BASE_URL = 'http://localhost:8108';
  private BASE_URL = environment.apiUrl;

  private CONTACTS_URL = `${this.BASE_URL}chat/user/getContactList/:id`;
  private CONVERSATION_URL = `${this.BASE_URL}chat/user/conversation`;

  private _conversations: ChatConversation[] = [];
  private _contacts: ChatUser[] = [];

  private get userId(): number {
    return this.userService.userInfo().userInfoMstrId;
  }

  constructor(
    private http: HttpClient,
    private userService: UserService
  ) { }

  connect() {
    if (this.userId) {

      this.connection = io.connect(this.BASE_URL, {
        path: '/chat/socket/socket.io',
        query: {
          userId: this.userId
        }
      });

      this.obserbeSocketConnection();
    } else {
      // TODO fix this when userInfo its set properly
      setTimeout(() => { this.connect(); }, 100);
    }
  }

  getCurrentUser(): ChatUser {
    let userInfo = this.userService.userInfo();
    return {
      fullName: userInfo.fullName,
      id: userInfo.userInfoMstrId,
      status: ChatStatus.ACTIVE
    } as ChatUser;
  }

  /**
   * Request a conversation from the socket
   * @param conversationId id to get throug the socket
   */
  getConversation(conversationId: number) {
    return new Observable(obs => {
      this.http.get<any>(this.CONVERSATION_URL + `/${conversationId}`).subscribe(data => {
        let getConversation = this.responseToChatConversation(data);
        let conv = this.findConversationById(getConversation.id);

        if (conv) {
          conv.messages = getConversation.messages;
          obs.next(conv);
        } else {
          this._conversations.push(getConversation);
          obs.next(getConversation);
        }
      });
    });
  }

  /**
   * Creates a conversation between the current user and another
   * @param user2_id user 2 for converstion
   */
  startConversation(toUserId) {
    return new Observable(obs => {
      this.http.post<any>(this.CONVERSATION_URL, { user1_id: this.userId, user2_id: toUserId }).subscribe(data => {
        // set the converstion on the contact
        let contact = this.findContactById(toUserId);
        if (contact) {
          contact.conversationId = data.id;
        }

        // continue regular flow
        let conversation = this.responseToChatConversation(data);
        this._conversations.push(conversation);
        obs.next(conversation);
      });
    });
  }

  /**
   * Socket action that will send a message to a given conversation
   * @param conversationId Id of the conversation of the message
   * @param message ChatMessage that needs to be sent to the conversation
   */
  sendMessage(fromId: number, toId: number, message: ChatMessage) {
    this.log('sending message', { chatDetails: { fromId, toId, msg: message.message } });
    this.connection.emit('new-chat-msg', { chatDetails: { fromId, toId, msg: message.message } });
  }

  getContacts(userId: number) {
    let that = this;
    return new Observable(obs => {
      this.retriveContacts(userId)
        .subscribe(contacts => {
          this._contacts = contacts;
          obs.next(contacts);

          // this.log(contacts);
        });
    });
  }

  // listen to socket events
  private obserbeSocketConnection() {
    // this.connection.on('recive-contacts', (response: Contacts) => {
    //   this.onContactsRecived(response.data);
    // });

    this.connection.on('show-msg', (response: NewMessage) => {
      this.log(response);
      this.log('conversations', this._conversations);
      let conversation = this.findConversationByUserId(response.chatDetails.toId) || this.findConversationByUserId(response.chatDetails.fromId);

      if (conversation) {
        this.log('conversation', conversation);

        let message = {
          createdAt: new Date(Date.now()),
          message: response.chatDetails.msg,
          type: ChatMessageType.TEXT,
          userId: response.chatDetails.fromId
        } as ChatMessage;

        this.onMessageRecived(conversation.id, message);
      } else {
        console.error('chat-service:', 'no conversation found');
      }
    });

    this.connection.on('user-disconnected', (userId: number) => {
      this.onUserStatusUpdate(userId, ChatStatus.NOT_AVAILABLE);
    });

    this.connection.on('user-connected', (userId: number) => {
      this.onUserStatusUpdate(userId, ChatStatus.ACTIVE);
    });

    this.connection.on('new-conversation', (conversation: ChatConversation) => {
      this.onConversationRecived(conversation);
    });

    this.log('listening to socket events');
  }

  private responseToChatConversation(serverResponse) {
    let contact = this.findContactById(serverResponse.user1_id) || this.findContactById(serverResponse.user2_id);

    this.log('contact found?', contact);
    // set this conversation as the conversation whit the contact found
    contact.conversationId = serverResponse.id;

    return {
      id: serverResponse.id,
      messages: serverResponse.messages,
      user: contact
    } as ChatConversation
  }

  // Sockets events

  /**
   * This handles messages recived throug the backend
   * 
   * It will be called from the backed for messages sent and recived
   * so we know when to update the UI
   * @param conversationId Conversation Id of the message
   * @param message Chat Message sent from the backend
   */
  private onMessageRecived(conversationId: number, message: ChatMessage) {
    let conversation = this.findConversationById(conversationId);
    conversation.messages.push(message);
    conversation.user.lastMessage = message;
  }

  // /**
  //  * Recive the list of conctacts from the socket and emit it on 
  //  * the bheaivor subject so the components can listen to it;
  //  * @param contacts Array of chat users coming from the backend
  //  */
  // private onContactsRecived(contacts: ChatUser[]) {
  //   this._contacts = contacts;
  // }

  /**
   * Handle Updates on contacts status
   * @param userId User id of the user that had an status change
   * @param status new ChatStatus for the user
   */
  private onUserStatusUpdate(userId: number, status: ChatStatus) {
    let contact = this.findContactById(userId);

    if (contact) {
      contact.status = status;
    }
  }

  private onConversationRecived(conv) {
    // if its already added ignore this (only happens for user)
    // who started the conversation
    if (this.findConversationById(conv.id)) {
      return;
    }


    // find the rigth contact
    let userId = this.userId === conv.user1_id ? conv.user2_id : conv.user1_id;

    // set the converstion on the contact
    let contact = this.findContactById(userId);

    if (contact) {
      this.addConversation(conv);
    } else {
      this.log('new conversation found with user', userId);
      // if there is no contact retrive the contacts again to
      // be able to set the conversation
      this.retriveContacts(this.userId).subscribe(contacts => {
        this.addNewContacts(contacts);
        this.addConversation(conv);
      });
    }
  }

  /**
   * Adds a converstion sent by the server into the conversations array if its
   * not there already
   * 
   * Before adding it, we convert the server response into a chat conversation
   * @param conv new conversation sent by the server
   */
  private addConversation(conv) {
    if (!this.findConversationById(conv.id)) {
      let conversation = this.responseToChatConversation(conv);
      this._conversations.push(conversation);
    }
  }

  /**
   * Adds all the new contacts passed in the array of contacts
   * but first it makes sure that they are not in the contacts array
   * @param contacts list of contacts returned by the server
   */
  private addNewContacts(contacts: {}[]) {
    // find which ones are the new contacts
    let newContacts = _.differenceBy(contacts, this._contacts, 'id');
    this.log('adding new contacts', newContacts);

    this._contacts.push(...newContacts);
  }

  // TODO handle socket disconnection

  // Utilities
  private findConversationById(id): ChatConversation {
    return this._conversations.find(conversation => conversation.id == id);
  }

  private findConversationByUserId(id: number): ChatConversation {
    return this._conversations.find(conversation => conversation.user.id == id);
  }

  private findContactById(id): ChatUser {
    return this._contacts.find(contact => contact.id == id);
  }

  private log(...args) {
    console.log('chat-service:', ...args);
  }

  private retriveContacts(userId: number) {
    return this.http.get<any>(this.CONTACTS_URL.replace(':id', userId.toString()));
  }
}
