package model

import (
	"cwtch.im/cwtch/model/attr"
	"cwtch.im/cwtch/model/constants"
	"encoding/json"
	"fmt"
	"git.openprivacy.ca/openprivacy/connectivity/tor"
	"git.openprivacy.ca/openprivacy/log"
	"time"
)

// AccessControl is a type determining client assigned authorization to a peer
// for a given conversation
type AccessControl struct {
	Blocked bool // Any attempts from this handle to connect are blocked overrides all other settings

	// Basic Conversation Rights
	Read   bool // Allows a handle to access the conversation
	Append bool // Allows a handle to append new messages to the conversation

	AutoConnect        bool // Profile should automatically try to connect with peer
	ExchangeAttributes bool // Profile should automatically exchange attributes like Name, Profile Image, etc.

	// Extension Related Permissions
	ShareFiles   bool // Allows a handle to share files to a conversation
	RenderImages bool // Indicates that certain filetypes should be autodownloaded and rendered when shared by this contact

	ManageGroup bool // Allows this conversation to be managed by hybrid groups
}

// DefaultP2PAccessControl defaults to a semi-trusted peer with no access to special extensions.
func DefaultP2PAccessControl() AccessControl {
	return AccessControl{Read: true, Append: true, ExchangeAttributes: true, Blocked: false,
		AutoConnect: true, ShareFiles: false, RenderImages: false}
}

// NoAccessControl defaults to a none-trusted peer with no access to special extensions.
// This is used as a fall back (if due to a software glitch a contact was setup without an access control, or for
// special contacts that should never be involvedt in external networking e.g. notes-to-self, or managed peers)
func NoAccessControl() AccessControl {
	return AccessControl{Read: false, Append: false, ExchangeAttributes: false, Blocked: false,
		AutoConnect: false, ShareFiles: false, RenderImages: false}
}

// AccessControlList represents an access control list for a conversation. Mapping handles to conversation
// functions
type AccessControlList map[string]AccessControl

// Serialize transforms the ACL into json.
func (acl *AccessControlList) Serialize() []byte {
	data, _ := json.Marshal(acl)
	return data
}

// DeserializeAccessControlList takes in JSON and returns an AccessControlList
func DeserializeAccessControlList(data []byte) (AccessControlList, error) {
	var acl AccessControlList
	err := json.Unmarshal(data, &acl)
	return acl, err
}

// Attributes a type-driven encapsulation of an Attribute map.
type Attributes map[string]string

// Serialize transforms an Attributes map into a JSON struct
func (a *Attributes) Serialize() []byte {
	data, _ := json.Marshal(a)
	return data
}

// DeserializeAttributes converts a JSON struct into an Attributes map
func DeserializeAttributes(data []byte) Attributes {
	attributes := make(Attributes)
	err := json.Unmarshal(data, &attributes)
	if err != nil {
		log.Error("error deserializing attributes (this is likely a programming error): %v", err)
		return make(Attributes)
	}
	return attributes
}

// Conversation encapsulates high-level information about a conversation, including the
// handle, any set attributes, the access control list associated with the message tree and the
// accepted status of the conversation (whether the user has consented into the conversation).
type Conversation struct {
	ID         int
	Handle     string
	Attributes Attributes
	ACL        AccessControlList

	// Deprecated, please use ACL for permissions related functions
	Accepted bool
}

// GetAttribute is a helper function that fetches a conversation attribute by scope, zone and key
func (ci *Conversation) GetAttribute(scope attr.Scope, zone attr.Zone, key string) (string, bool) {
	if value, exists := ci.Attributes[scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key)).ToString()]; exists {
		return value, true
	}
	return "", false
}

// GetPeerAC returns a suitable Access Control object for a the given peer conversation
// If this is called for a group conversation, this method will error and return a safe default AC.
func (ci *Conversation) GetPeerAC() AccessControl {
	if acl, exists := ci.ACL[ci.Handle]; exists {
		return acl
	}
	log.Errorf("attempted to access a Peer Access Control object from %v but peer ACL is undefined. This is likely a programming error - fallback to a NoAccess AC", ci.Handle)
	return NoAccessControl()
}

// HasChannel returns true if the requested channel has been setup for this conversation
func (ci *Conversation) HasChannel(requestedChannel int) bool {
	if requestedChannel == 0 {
		return true
	}
	if requestedChannel == 1 {
		return false // channel 1 is mapped to channel 0 for backwards compatibility
	}
	key := fmt.Sprintf("channel.%d", requestedChannel)
	if value, exists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(key)).ToString()]; exists {
		return value == constants.True
	}
	return false
}

// IsCwtchPeer is a helper attribute that identifies whether a conversation is a cwtch peer
func (ci *Conversation) IsCwtchPeer() bool {
	return tor.IsValidHostname(ci.Handle)
}

// IsGroup is a helper attribute that identifies whether a conversation is a legacy group
func (ci *Conversation) IsGroup() bool {
	if _, exists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupID)).ToString()]; exists {
		return true
	}
	return false
}

// IsServer is a helper attribute that identifies whether a conversation is with a server
func (ci *Conversation) IsServer() bool {
	if _, exists := ci.Attributes[attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(BundleType))).ToString()]; exists {
		return true
	}
	return false
}

// ServerSyncProgress is only valid during a server being in the AUTHENTICATED state and therefor in the syncing process
// it returns a double (0-1) representing the estimated progress of the syncing
func (ci *Conversation) ServerSyncProgress() float64 {
	startTimeStr, startExists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.SyncPreLastMessageTime)).ToString()]
	recentTimeStr, recentExists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.SyncMostRecentMessageTime)).ToString()]

	if !startExists || !recentExists {
		return 0.0
	}

	startTime, err := time.Parse(startTimeStr, time.RFC3339Nano)
	if err != nil {
		return 0.0
	}
	recentTime, err := time.Parse(recentTimeStr, time.RFC3339Nano)
	if err != nil {
		return 0.0
	}

	syncRange := time.Since(startTime)
	pointFromStart := startTime.Sub(recentTime)
	return pointFromStart.Seconds() / syncRange.Seconds()
}

// ConversationMessage bundles an instance of a conversation message row
type ConversationMessage struct {
	ID          int
	Body        string
	Attr        Attributes
	Signature   string
	ContentHash string
}
