var mlib = require('ssb-msgs')
var u = require('./util')

module.exports = function (sbot, db, state, emit) {

  var processors = {
    post: function (msg) {
      var me = getProfile(sbot.id)
      var author = msg.value.author
      var by_me = (author === sbot.id)
      var c = msg.value.content
      
      // home index
      if (c.recps)
        return // skip targeted messages

      state.home.sortedUpsert(msg.value.timestamp, msg.key)
      if (mlib.link(c.root, 'msg')) {
        // a reply, put its *parent* in the home index
        state.pinc()
        u.getRootMsg(sbot, msg, function (err, rootmsg) {
          if (rootmsg && typeof rootmsg.value.content != 'string') // dont put encrypted msgs in homestream
            state.home.sortedUpsert(rootmsg.value.timestamp, rootmsg.key)
          state.pdec()            
        })
      }

      if (!by_me) {
        // emit home-add if by a followed user and in the last hour
        if (follows(sbot.id, author) && ((Date.now() - msg.value.timestamp) < 1000*60*60))
          emit('home-add')
      }

      // inbox index
      if (!by_me) {
        var inboxed = false
        mlib.links(c.root, 'msg').forEach(function (link) {
          if (inboxed) return
          // a reply to my messages?
          if (state.mymsgs.indexOf(link.link) >= 0) {
            var row = state.inbox.sortedInsert(msg.value.timestamp, msg.key)
            attachIsRead(row)
            row.author = msg.value.author // inbox index is filtered on read by the friends graph
            if (follows(sbot.id, row.author))
              emit('index-change', { index: 'inbox' })
            inboxed = true
          }
        })
        mlib.links(c.mentions, 'feed').forEach(function (link) {
          if (inboxed) return
          // mentions me?
          if (link.link == sbot.id) {
            var row = state.inbox.sortedInsert(msg.value.timestamp, msg.key)
            attachIsRead(row)
            row.author = msg.value.author // inbox index is filtered on read by the friends graph
            if (follows(sbot.id, row.author))
              emit('index-change', { index: 'inbox' })
            inboxed = true
          }
        })
      }
    },

    site: function (msg) {
      var site = getSite(msg.value.author)

      // additions
      mlib.links(msg.value.content.includes, 'blob').forEach(function (link) {
        if (!link.path)
          return
        site[link.path] = link
      })

      // removals
      var excludes = msg.value.content.excludes
      if (excludes) {
        ;(Array.isArray(excludes) ? excludes : [excludes]).forEach(function (item) {
          if (!item.path)
            return
          delete site[item.path]
        })
      }
    },

    contact: function (msg) {
      // update profiles
      mlib.links(msg.value.content.contact, 'feed').forEach(function (link) {
        var toself = link.link === msg.value.author
        if (toself) updateSelfContact(msg.value.author, msg)
        else        updateOtherContact(msg.value.author, link.link, msg)
      })
    },

    about: function (msg) {
      // update profiles
      mlib.links(msg.value.content.about, 'feed').forEach(function (link) {
        var toself = link.link === msg.value.author
        if (toself) updateSelfContact(msg.value.author, msg)
        else        updateOtherContact(msg.value.author, link.link, msg)
      })
    },

    vote: function (msg) {
      // update tallies
      var link = mlib.link(msg.value.content.vote, 'msg')

      if (link) {
        if (msg.value.author == sbot.id)
          updateMyVote(msg, link)
        else if (state.mymsgs.indexOf(link.link) >= 0) // vote on my msg?
          updateVoteOnMymsg(msg, link)
      }
    },

    flag: function (msg) {
      // inbox index
      var link = mlib.link(msg.value.content.flag, 'msg')
      if (sbot.id != msg.value.author && link && state.mymsgs.indexOf(link.link) >= 0) {
        var row = state.inbox.sortedInsert(msg.value.timestamp, msg.key)
        attachIsRead(row)
        row.author = msg.value.author // inbox index is filtered on read by the friends graph
        if (follows(sbot.id, msg.value.author))
          emit('index-change', { index: 'inbox' })
      }

      // user flags
      var link = mlib.link(msg.value.content.flag, 'feed')
      if (link) {
        var source = getProfile(msg.value.author)
        var target = getProfile(link.link)

        var flag = link.reason ? { key: msg.key, reason: link.reason } : false
        source.assignedTo[target.id].flagged = flag
        target.assignedBy[source.id].flagged = flag

        // track if by local user
        if (source.id === sbot.id)
          target.flagged = flag
      }
    }
  }

  function getProfile (pid) {
    if (pid.id) // already a profile?
      return pid

    var profile = state.profiles[pid]
    if (!profile) {
      state.profiles[pid] = profile = {
        id: pid,

        // current values...
        self: { name: null, image: null }, // ...set by self about self
        assignedBy: {}, // ...set by others about self
        assignedTo: {}, // ...set by self about others

        // has local user flagged?
        flagged: false
      }
    }
    return profile
  }

  function getSite (pid) {
    var site = state.sites[pid]
    if (!site)
      state.sites[pid] = site = {}
    return site
  }

  function updateSelfContact (author, msg) {
    var c = msg.value.content
    author = getProfile(author)

    // name: a non-empty string
    if (nonEmptyStr(c.name)) {
      author.self.name = makeNameSafe(c.name)
      rebuildNamesFor(author)
    }

    // image: link to image
    if ('image' in c) {
      if (mlib.link(c.image, 'blob'))
        author.self.image = mlib.link(c.image)
      else if (!c.image)
        delete author.self.image
    }
  }

  function updateOtherContact (source, target, msg) {
    var c = msg.value.content
    source = getProfile(source)
    target = getProfile(target)
    source.assignedTo[target.id] = source.assignedTo[target.id] || {}
    target.assignedBy[source.id] = target.assignedBy[source.id] || {}
    var userProf = getProfile(sbot.id)

    // name: a non-empty string
    if (nonEmptyStr(c.name)) {
      source.assignedTo[target.id].name = makeNameSafe(c.name)
      target.assignedBy[source.id].name = makeNameSafe(c.name)
      rebuildNamesFor(target)
    }

    // following: bool
    if (typeof c.following === 'boolean') {
      source.assignedTo[target.id].following = c.following
      target.assignedBy[source.id].following = c.following

      // if from the user, update names (in case un/following changes conflict status)
      if (source.id == sbot.id)
        rebuildNamesFor(target)

      // follows index
      if (target.id == sbot.id) {
        // use the follower's id as the key to this index, so we only have 1 entry per other user max
        var row = state.follows.sortedUpsert(msg.value.timestamp, msg.key)
        row.following = c.following
        attachIsRead(row, msg.key)
      }
    }

    // blocking: bool
    if (typeof c.blocking === 'boolean') {
      source.assignedTo[target.id].blocking = c.blocking
      target.assignedBy[source.id].blocking = c.blocking
    }
  }

  function rebuildNamesFor (profile) {
    profile = getProfile(profile)

    // remove oldname from id->name map
    var oldname = state.names[profile.id]
    if (oldname) {
      if (state.ids[oldname] == profile.id) {
        // remove
        delete state.ids[oldname]
      } else if (Array.isArray(state.ids[oldname])) {
        // is in a conflict, remove from conflict array
        var i = state.ids[oldname].indexOf(profile.id)
        if (i !== -1) {
          state.ids[oldname].splice(i, 1)
          if (state.ids[oldname].length === 1) {
            // conflict resolved
            delete state.actionItems[oldname]
            state.ids[oldname] = state.ids[oldname][0]
          }
        }
      }
    }

    // default to self-assigned name
    var name = profile.self.name
    if (profile.id !== sbot.id && profile.assignedBy[sbot.id] && profile.assignedBy[sbot.id].name) {
      // use name assigned by the local user, if one is given
      name = profile.assignedBy[sbot.id].name
    }
    if (!name)
      return

    // store
    state.names[profile.id] = name

    // if following, update id->name map
    if (profile.id === sbot.id || profile.assignedBy[sbot.id] && profile.assignedBy[sbot.id].following) {
      if (!state.ids[name]) { // no conflict?
        // take it
        state.ids[name] = profile.id
      } else {
        // keep track of all assigned ids
        if (Array.isArray(state.ids[name]))
          state.ids[name].push(profile.id)
        else
          state.ids[name] = [state.ids[name], profile.id]
        // conflict, this needs to be handled by the user
        state.actionItems[name] = {
          type: 'name-conflict',
          name: name,
          ids: state.ids[name]
        }
      }
    }
  }

  function updateMyVote (msg, l) {
    // myvotes index
    var row = state.myvotes.sortedUpsert(msg.value.timestamp, l.link)
    row.vote = l.value
  }

  function updateVoteOnMymsg (msg, l) {
    // votes index
    // construct a composite key which will be the same for all votes by this user on the given target
    var votekey = l.link + '::' + msg.value.author // lonnng fucking key
    var row = state.votes.sortedUpsert(msg.value.timestamp, votekey)
    row.vote = l.value
    row.votemsg = msg.key
    if (row.vote > 0) attachIsRead(row, msg.key)
    else              row.isread = true // we dont care about non-upvotes
  }

  function attachIsRead (indexRow, key) {
    key = key || indexRow.key
    state.pinc()
    db.isread.get(key, function (err, v) {
      state.pdec()
      indexRow.isread = !!v
    })
  }

  function follows (a, b) {
    var aT = getProfile(a).assignedTo[b]
    return (a != b && aT && aT.following)
  }

  // exported api

  function fn (logkey) {
    state.pinc()
    var key = logkey.value
    sbot.get(logkey.value, function (err, value) {
      var msg = { key: key, value: value }
      try {
        // encrypted? try to decrypt
        if (typeof value.content == 'string' && value.content.slice(-4) == '.box') {
          value.content = sbot.private.unbox(value.content)
          if (!value.content)
            return state.pdec()

          // put all decrypted messages in the inbox index
          var row = state.inbox.sortedInsert(msg.value.timestamp, msg.key)
          attachIsRead(row)
          row.author = msg.value.author // inbox index is filtered on read by the friends graph
          if (follows(sbot.id, msg.value.author))
            emit('index-change', { index: 'inbox' })
        }

        // collect keys of user's messages
        if (msg.value.author === sbot.id)
          state.mymsgs.push(msg.key)

        // type processing
        var process = processors[msg.value.content.type]
        if (process)
          process(msg)
      }
      catch (e) {
        // :TODO: use sbot logging plugin
        console.error('Failed to process message', e, e.stack, key, value)
      }
      state.pdec()
    })
  }

  return fn
}

function nonEmptyStr (str) {
    return (typeof str === 'string' && !!(''+str).trim())
  }

// allow A-z0-9._-, dont allow a trailing .
var badNameCharsRegex = /[^A-z0-9\._-]/g
function makeNameSafe (str) {
  str = str.replace(badNameCharsRegex, '_')
  if (str.charAt(str.length - 1) == '.')
    str = str.slice(0, -1) + '_'
  return str
}