package ch.threema.storage.models;

import android.text.format.DateUtils;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.slf4j.Logger;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Instant;

import ch.threema.app.utils.TextUtil;
import ch.threema.data.datatypes.AvailabilityStatus;
import ch.threema.data.datatypes.ContactNameFormat;
import ch.threema.data.datatypes.ContactNotificationTriggerPolicyOverride;
import ch.threema.data.datatypes.ConversationVisibility;
import ch.threema.data.datatypes.IdColor;
import ch.threema.base.crypto.NaCl;
import ch.threema.app.preference.service.PreferenceService;

import static ch.threema.base.utils.LoggingKt.getThreemaLogger;

import ch.threema.domain.models.AcquaintanceLevel;
import ch.threema.domain.models.Contact;
import ch.threema.domain.models.IdentityState;
import ch.threema.domain.models.IdentityType;
import ch.threema.domain.models.VerificationLevel;
import ch.threema.domain.models.WorkVerificationLevel;
import ch.threema.domain.protocol.csp.ProtocolDefines;

import static ch.threema.app.utils.TextUtil.SPACE;
import static ch.threema.app.utils.TextUtil.TILDE;

@Deprecated
public class ContactModel extends Contact implements ReceiverModel {

    private final static Logger logger = getThreemaLogger("ContactModel");

    public static final String TABLE = "contacts";
    public static final String COLUMN_IDENTITY = "identity";
    public static final String COLUMN_PUBLIC_KEY = "publicKey";
    public static final String COLUMN_FIRST_NAME = "firstName";
    public static final String COLUMN_LAST_NAME = "lastName";
    public static final String COLUMN_PUBLIC_NICK_NAME = "publicNickName";
    public static final String COLUMN_VERIFICATION_LEVEL = "verificationLevel";
    public static final String COLUMN_ANDROID_CONTACT_LOOKUP_KEY = "androidContactId"; /* The complete lookup key (consisting of key and ID) of the android contact this contact is linked with */
    public static final String COLUMN_FEATURE_MASK = "featureLevel";
    public static final String COLUMN_STATE = "state";
    public static final String COLUMN_ID_COLOR_INDEX = "idColorIndex";
    public static final String COLUMN_LOCAL_AVATAR_EXPIRES = "avatarExpires";
    public static final String COLUMN_IS_WORK = "isWork";
    public static final String COLUMN_TYPE = "type";
    public static final String COLUMN_PROFILE_PIC_BLOB_ID = "profilePicBlobID"; /* the blob ID of the profile pic that was last sent to this contact */
    public static final String COLUMN_CREATED_AT = "dateCreated"; /* date when this contact was created locally */
    public static final String COLUMN_LAST_UPDATE_AT = "lastUpdateAt"; /* date when the conversation was last updated */
    public static final String COLUMN_ACQUAINTANCE_LEVEL = "acquaintanceLevel";
    public static final String COLUMN_IS_RESTORED = "isRestored"; /* whether this contact has been restored from a backup and not yet been contacted */
    public static final String COLUMN_CONVERSATION_VISIBILITY = "conversationVisibility";
    public static final String COLUMN_READ_RECEIPTS = "readReceipts"; /* whether read receipts should be sent to this contact */
    public static final String COLUMN_TYPING_INDICATORS = "typingIndicators"; /* whether typing indicators should be sent to this contact */
    public static final String COLUMN_FORWARD_SECURITY_STATE = "forwardSecurityState"; /* current state of forward security with this contact */
    public static final String COLUMN_SYNC_STATE = "syncState"; /* contact synchronization state, 0: INITIAL, 1: IMPORTED, 2: CUSTOM */
    public static final String COLUMN_JOB_TITLE = "jobTitle";
    public static final String COLUMN_DEPARTMENT = "department";
    public static final String COLUMN_NOTIFICATION_TRIGGER_POLICY_OVERRIDE_POLICY = "notificationTriggerPolicyOverridePolicy";
    public static final String COLUMN_NOTIFICATION_TRIGGER_POLICY_OVERRIDE_EXPIRES_AT = "notificationTriggerPolicyOverrideExpiresAt";
    public static final String COLUMN_WORK_LAST_FULL_SYNC_AT = "workLastFullSyncAt";

    public static final byte[] NO_PROFILE_PICTURE_BLOB_ID = new byte[0];

    /**
     * Policy for sending read receipts or typing indicators
     */
    public static final int DEFAULT = 0; // use the global setting
    public static final int SEND = 1; // always send, regardless of global setting
    public static final int DONT_SEND = 2; // never send

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({DEFAULT, SEND, DONT_SEND})
    public @interface OverridePolicy {
    }

    /**
     * Forward Security state constants. Note that these values are only maintained for contacts
     * with a DH session of version 1.0.
     */
    public static final int FS_OFF = 0; // last message from this contact did not have FS enabled
    public static final int FS_ON = 1; // last message from this contact was received with FS

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({FS_OFF, FS_ON})
    public @interface ForwardSecurityState {
    }

    // Timeout for avatars of linked contacts
    public static long DEFAULT_ANDROID_CONTACT_AVATAR_EXPIRY = DateUtils.DAY_IN_MILLIS * 14;

    private String publicNickName;
    private IdentityState state;
    private String androidContactId;
    private long featureMask;
    private @NonNull IdColor idColor = IdColor.invalid();
    private boolean isWork, isRestored;
    @NonNull
    private ConversationVisibility conversationVisibility = ConversationVisibility.NORMAL;
    private AcquaintanceLevel acquaintanceLevel = AcquaintanceLevel.DIRECT;
    private Instant localAvatarExpires, dateCreated;
    private @Nullable Instant lastUpdate;
    private byte[] profilePicBlobID;
    private @Nullable IdentityType type;
    private @OverridePolicy int readReceipts, typingIndicators;
    // TODO(ANDR-2452): Remove the forward security state when most of clients support 1.1 anyway
    private int forwardSecurityState;
    private @Nullable String jobTitle;
    private @Nullable String department;
    private @Nullable ContactNotificationTriggerPolicyOverride notificationTriggerPolicyOverride;
    private @NonNull AvailabilityStatus availabilityStatus = AvailabilityStatus.None.INSTANCE;

    private ContactModel(String identity, @NonNull byte[] publicKey) {
        super(identity, publicKey, VerificationLevel.UNVERIFIED);
    }

    /**
     * Create a contact model with the given identity and public key. Note that this method does not
     * check that the identity is a valid string or that the public key contains
     * {@code NaCl.PUBLIC_KEY_BYTES} bytes.
     * This method should only be used when constructing a contact model from the database as it may
     * contain invalid data. Use {@link #create(String, byte[])} whenever creating a new contact.
     */
    @NonNull
    public static ContactModel createUnchecked(@NonNull String identity, @NonNull byte[] publicKey) {
        if (identity.length() != ProtocolDefines.IDENTITY_LEN) {
            logger.warn("Creating contact with invalid identity: {}", identity);
        }
        if (publicKey.length != NaCl.PUBLIC_KEY_BYTES) {
            logger.warn("Creating contact {} with invalid public key of length {}", identity, publicKey.length);
        }
        return new ContactModel(identity, publicKey);
    }

    /**
     * Create a contact model with the given identity and public key.
     *
     * @throws IllegalArgumentException if the identity or the public key is of invalid length
     */
    @NonNull
    public static ContactModel create(@NonNull String identity, @NonNull byte[] publicKey) {
        if (identity.length() != ProtocolDefines.IDENTITY_LEN) {
            throw new IllegalArgumentException("Invalid identity: " + identity);
        }
        if (publicKey.length != NaCl.PUBLIC_KEY_BYTES) {
            throw new IllegalArgumentException("Invalid public key of length " + publicKey.length);
        }

        return new ContactModel(identity, publicKey);
    }

    /**
     * Return whether or not this contact is linked to an Android contact.
     */
    public boolean isLinkedToAndroidContact() {
        return this.getAndroidContactLookupKey() != null;
    }

    /**
     * Get the complete lookup key (consisting of key with appended ID) of the android contact this contact is linked with
     *
     * @return lookup key to be used for a contact lookup
     */
    @Nullable
    public String getAndroidContactLookupKey() {
        return androidContactId;
    }

    public ContactModel setAndroidContactLookupKey(String androidContactId) {
        this.androidContactId = androidContactId;
        // degrade verification level as this contact is no longer connected to an address book contact
        if (androidContactId == null && verificationLevel == VerificationLevel.SERVER_VERIFIED) {
            verificationLevel = VerificationLevel.UNVERIFIED;
        }
        return this;
    }

    public ContactModel setName(String firstName, String lastName) {
        this.setLastName(lastName);
        this.setFirstName(firstName);
        return this;
    }

    public String getPublicNickName() {
        return this.publicNickName;
    }

    public ContactModel setPublicNickName(String publicNickName) {
        this.publicNickName = publicNickName;
        return this;
    }

    /**
     * Call this to set id color index. If this has not been called or the value is negative and the
     * color is being accessed, the hash will be recomputed to get the color index.
     *
     * @param colorIndex the id color index
     * @return this contact model
     */
    public ContactModel setIdColorIndex(int colorIndex) {
        this.idColor = new IdColor(colorIndex);
        return this;
    }

    /**
     * Get the id color. If the value is not initialized, this object will be updated and the
     * computed value will be returned.
     *
     * @return the id color of the contact
     */
    @NonNull
    public IdColor getIdColor() {
        if (!this.idColor.isValid()) {
            idColor = IdColor.ofIdentity(getIdentity());
        }
        return idColor;
    }

    public long getFeatureMask() {
        return featureMask;
    }

    public ContactModel setFeatureMask(long featureMask) {
        this.featureMask = featureMask;
        return this;
    }

    public IdentityState getState() {
        return this.state;
    }

    public ContactModel setState(IdentityState state) {
        this.state = state;
        return this;
    }

    /**
     * Get the expiration date of a local avatar (either a gateway contact avatar,
     * or the avatar of a contact linked to an Android system contact).
     */
    public @Nullable Instant getLocalAvatarExpires() {
        return localAvatarExpires;
    }

    /**
     * Update the expiration date of a local avatar (either a gateway contact avatar,
     * or the avatar of a contact linked to an Android system contact).
     */
    public @NonNull ContactModel setLocalAvatarExpires(@Nullable Instant avatarExpires) {
        this.localAvatarExpires = avatarExpires;
        return this;
    }

    /**
     * Set this flag, if the contact is "work verified", i.e. has been added to the contact list in the management cockpit
     * "work verified" contacts are symbolized by a blue verification level
     *
     * @param isWork true if the contact is "work verified", false otherwise
     * @return ContactModel for chaining calls
     */
    public ContactModel setIsWork(boolean isWork) {
        this.isWork = isWork;
        return this;
    }

    /**
     * Check whether the contact is "work verified", i.e. has been added to the contact list in the management cockpit
     *
     * @return true if the contact is "work verified", false otherwise
     */
    public boolean isWorkVerified() {
        return this.isWork;
    }

    /**
     * Get the work verification level of the contact. If {@link #isWork} is true,
     * {@link WorkVerificationLevel#WORK_SUBSCRIPTION_VERIFIED} is returned,
     * {@link WorkVerificationLevel#NONE} otherwise.
     */
    @NonNull
    public WorkVerificationLevel getWorkVerificationLevel() {
        if (isWork) {
            return WorkVerificationLevel.WORK_SUBSCRIPTION_VERIFIED;
        } else {
            return WorkVerificationLevel.NONE;
        }
    }

    /**
     * Get the BlobId of the latest profile picture that was sent to this contact.
     *
     * @return The blobId of the latest profile-picture sent to this contact, {@code null} if no
     * profile-picture has been sent or {@code new byte[0]} if a delete-profile-picture message has been sent
     */
    @Nullable
    public byte[] getProfilePicBlobID() {
        return profilePicBlobID;
    }

    /**
     * Set the {@link AcquaintanceLevel} of the contact.
     */
    public ContactModel setAcquaintanceLevel(@NonNull AcquaintanceLevel acquaintanceLevel) {
        this.acquaintanceLevel = acquaintanceLevel;
        return this;
    }

    /**
     * Return the current {@link AcquaintanceLevel} of the contact.
     */
    public @NonNull AcquaintanceLevel getAcquaintanceLevel() {
        return this.acquaintanceLevel;
    }

    @Override
    public boolean isHidden() {
        // Hide chat if acquaintance level with this contact is set to GROUP_OR_DELETED
        return this.acquaintanceLevel == AcquaintanceLevel.GROUP_OR_DELETED;
    }

    public ContactModel setIsRestored(boolean isRestored) {
        this.isRestored = isRestored;
        return this;
    }

    public boolean isRestored() {
        return this.isRestored;
    }

    /**
     * Set the BlobId of the latest profile picture that was sent to this contact.
     *
     * @param profilePicBlobID The blobId of the latest profile-picture sent to this contact, {@code null} if no
     *                         profile-picture has been sent or {@code new byte[0]} if a delete-profile-picture message has been sent
     */
    public ContactModel setProfilePicBlobID(@Nullable byte[] profilePicBlobID) {
        this.profilePicBlobID = profilePicBlobID;
        return this;
    }

    public ContactModel setDateCreated(Instant dateCreated) {
        this.dateCreated = dateCreated;
        return this;
    }

    public @Nullable Instant getDateCreated() {
        return dateCreated;
    }

    @Override
    public ContactModel setLastUpdate(@Nullable Instant lastUpdate) {
        this.lastUpdate = lastUpdate;
        return this;
    }

    @Override
    public @Nullable Instant getLastUpdate() {
        return this.lastUpdate;
    }

    /**
     * Set the {@link IdentityType} (regular or work).
     */
    public ContactModel setIdentityType(IdentityType type) {
        this.type = type;
        return this;
    }

    /**
     * Return the {@link IdentityType} (regular or work).
     */
    public @Nullable IdentityType getIdentityType() {
        return this.type;
    }

    @Override
    public boolean isArchived() {
        return conversationVisibility == ConversationVisibility.ARCHIVED;
    }

    @NonNull
    public ConversationVisibility getConversationVisibility() {
        return this.conversationVisibility;
    }

    public ContactModel setConversationVisibility(@NonNull ConversationVisibility conversationVisibility) {
        this.conversationVisibility = conversationVisibility;
        return this;
    }

    public @OverridePolicy int getReadReceipts() {
        return readReceipts;
    }

    public ContactModel setReadReceipts(@OverridePolicy int readReceipts) {
        this.readReceipts = readReceipts;
        return this;
    }

    public @OverridePolicy int getTypingIndicators() {
        return typingIndicators;
    }

    public ContactModel setTypingIndicators(@OverridePolicy int typingIndicators) {
        this.typingIndicators = typingIndicators;
        return this;
    }

    /**
     * Get the forward security state of this contact. Note that these states are only maintained
     * for contacts with a DH session of version 1.0.
     * TODO(ANDR-2452): Remove the forward security state when most of clients support 1.1 anyway
     *
     * @return the forward security state
     */
    @ForwardSecurityState
    public int getForwardSecurityState() {
        return forwardSecurityState;
    }

    /**
     * Set the forward security state of this contact. Note that these states are only maintained
     * for contacts with a DH session of version 1.0.
     * TODO(ANDR-2452): Remove the forward security state when most of clients support 1.1 anyway
     *
     * @param forwardSecurityState the forward security state
     * @return this contact model
     */
    public ContactModel setForwardSecurityState(@ForwardSecurityState int forwardSecurityState) {
        this.forwardSecurityState = forwardSecurityState;
        return this;
    }

    @Nullable
    public String getJobTitle() {
        return jobTitle;
    }

    public ContactModel setJobTitle(@Nullable String jobTitle) {
        this.jobTitle = jobTitle;
        return this;
    }

    @Nullable
    public String getDepartment() {
        return department;
    }

    public ContactModel setDepartment(@Nullable String department) {
        this.department = department;
        return this;
    }

    @Nullable
    public ContactNotificationTriggerPolicyOverride getNotificationTriggerPolicyOverride() {
        return notificationTriggerPolicyOverride;
    }

    public ContactModel setNotificationTriggerPolicyOverride(@Nullable ContactNotificationTriggerPolicyOverride notificationTriggerPolicyOverride) {
        this.notificationTriggerPolicyOverride = notificationTriggerPolicyOverride;
        return this;
    }

    @NonNull
    public AvailabilityStatus getAvailabilityStatus() {
        return this.availabilityStatus;
    }

    public ContactModel setAvailabilityStatus(@NonNull AvailabilityStatus availabilityStatus) {
        this.availabilityStatus = availabilityStatus;
        return this;
    }

    @Override
    @NonNull
    public String toString() {
        return "ContactModel(identity=" + this.getIdentity() + ", display=" + this.getContactListItemTextTopLeft(ContactNameFormat.DEFAULT) + ")";
    }

    /**
     * Priority: (first-/lastname) --> (~nickname) --> (identity) <br>
     *
     * @return Either the first/lastname, the nickname or the identity. Never returns empty string because the identity is always filled.
     */
    @NonNull
    public String getContactListItemTextTopLeft(@NonNull ContactNameFormat contactNameFormat) {
        final StringBuilder textTopLeftBuilder = new StringBuilder();
        if (getHasFirstOrLastName()) {
            final @Nullable String firstPart = contactNameFormat == ContactNameFormat.FIRSTNAME_LASTNAME ? getFirstName() : getLastName();
            final @Nullable String lastPart = contactNameFormat == ContactNameFormat.FIRSTNAME_LASTNAME ? getLastName() : getFirstName();
            if (firstPart != null && !firstPart.isBlank()) {
                textTopLeftBuilder.append(firstPart.trim()).append(SPACE);
            }
            if (lastPart != null && !lastPart.isBlank()) {
                textTopLeftBuilder.append(lastPart.trim());
            }
        } else if (publicNickName != null && !publicNickName.isBlank()) {
            textTopLeftBuilder.append(TILDE).append(publicNickName.trim());
        } else {
            textTopLeftBuilder.append(getIdentity().trim());
        }
        return textTopLeftBuilder.toString().trim();
    }

    /**
     * Priority: (job title) --> (nickname) --> empty <br><br>
     * Note that in this case the nickname would have no leading tilde
     *
     * @return Either the job title, the nickname or empty
     */
    @NonNull
    public String getContactListItemTextBottomLeft() {
        if (isWork && jobTitle != null && !jobTitle.isBlank()) {
            return jobTitle.trim();
        } else if (publicNickName != null && !publicNickName.isBlank()) {
            return TILDE + publicNickName.trim();
        } else {
            return "";
        }
    }

    /**
     * Priority: (department) --> (identity)
     *
     * @return Either the department or the identity. Never returns empty string because the identity is always filled.
     */
    @NonNull
    public String getContactListItemTextBottomRight() {
        if (isWork && department != null && !department.isBlank()) {
            return department.trim();
        } else {
            return getIdentity().trim();
        }
    }

    /**
     * The logical result is tied to the displayed values by the contact list item
     *
     * @see ch.threema.app.adapters.ContactListAdapter
     */
    public boolean matchesFilterQuery(
        final @NonNull PreferenceService preferenceService,
        final @NonNull String filterQuery
    ) {
        final @NonNull String contactTextTopLeft = getContactListItemTextTopLeft(preferenceService.getContactNameFormat());
        final @NonNull String contactTextBottomLeft = getContactListItemTextBottomLeft();
        final @NonNull String contactTextBottomRight = getContactListItemTextBottomRight();

        return TextUtil.matchesQueryDiacriticInsensitive(contactTextTopLeft, filterQuery) ||
            TextUtil.matchesQueryDiacriticInsensitive(contactTextBottomLeft, filterQuery) ||
            TextUtil.matchesQueryDiacriticInsensitive(contactTextBottomRight, filterQuery) ||
            (getIdentity().toUpperCase().contains(filterQuery.toUpperCase()));
    }
}


