// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {DeviceEventEmitter} from 'react-native';

import {ActionType, General, Navigation, Screens} from '@constants';
import DatabaseManager from '@database/manager';
import {getTranslations, t} from '@i18n';
import {getChannelById} from '@queries/servers/channel';
import {getPostById} from '@queries/servers/post';
import {getCurrentTeamId, getCurrentUserId, prepareCommonSystemValues, PrepareCommonSystemValuesArgs, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {addChannelToTeamHistory, addTeamToTeamHistory} from '@queries/servers/team';
import {getIsCRTEnabled, getThreadById, prepareThreadsFromReceivedPosts, queryThreadsInTeam} from '@queries/servers/thread';
import {getCurrentUser} from '@queries/servers/user';
import {dismissAllModalsAndPopToRoot, goToScreen} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';
import NavigationStore from '@store/navigation_store';
import {isTablet} from '@utils/helpers';
import {logError} from '@utils/log';
import {changeOpacity} from '@utils/theme';

import type Model from '@nozbe/watermelondb/Model';

export const switchToGlobalThreads = async (serverUrl: string, teamId?: string, prepareRecordsOnly = false) => {
    try {
        const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
        const models: Model[] = [];

        let teamIdToUse = teamId;
        if (!teamId) {
            teamIdToUse = await getCurrentTeamId(database);
        }

        if (!teamIdToUse) {
            throw new Error('no team to switch to');
        }

        await setCurrentTeamAndChannelId(operator, teamIdToUse, '');
        const history = await addChannelToTeamHistory(operator, teamIdToUse, Screens.GLOBAL_THREADS, true);
        models.push(...history);

        if (!prepareRecordsOnly) {
            await operator.batchRecords(models);
        }

        const isTabletDevice = await isTablet();
        if (isTabletDevice) {
            DeviceEventEmitter.emit(Navigation.NAVIGATION_HOME, Screens.GLOBAL_THREADS);
        } else {
            goToScreen(Screens.GLOBAL_THREADS, '', {}, {topBar: {visible: false}});
        }

        return {models};
    } catch (error) {
        logError('Failed switchToGlobalThreads', error);
        return {error};
    }
};

export const switchToThread = async (serverUrl: string, rootId: string, isFromNotification = false) => {
    try {
        const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
        const user = await getCurrentUser(database);
        if (!user) {
            throw new Error('User not found');
        }

        const post = await getPostById(database, rootId);
        if (!post) {
            throw new Error('Post not found');
        }
        const channel = await getChannelById(database, post.channelId);
        if (!channel) {
            throw new Error('Channel not found');
        }

        const currentTeamId = await getCurrentTeamId(database);
        const isTabletDevice = await isTablet();
        const teamId = channel.teamId || currentTeamId;

        let switchingTeams = false;
        if (currentTeamId === teamId) {
            const models = await prepareCommonSystemValues(operator, {
                currentChannelId: channel.id,
            });
            if (models.length) {
                await operator.batchRecords(models);
            }
        } else {
            const modelPromises: Array<Promise<Model[]>> = [];
            switchingTeams = true;
            modelPromises.push(addTeamToTeamHistory(operator, teamId, true));
            const commonValues: PrepareCommonSystemValuesArgs = {
                currentChannelId: channel.id,
                currentTeamId: teamId,
            };
            modelPromises.push(prepareCommonSystemValues(operator, commonValues));
            const models = (await Promise.all(modelPromises)).flat();
            if (models.length) {
                await operator.batchRecords(models);
            }
        }

        // Modal right buttons
        const rightButtons = [];

        const isCRTEnabled = await getIsCRTEnabled(database);
        if (isCRTEnabled) {
            // CRT: Add follow/following button
            rightButtons.push({
                id: 'thread-follow-button',
                component: {
                    id: post.id,
                    name: Screens.THREAD_FOLLOW_BUTTON,
                    passProps: {
                        teamId: channel.teamId,
                        threadId: post.id,
                    },
                },
            });
        }

        // Get translation by user locale
        const translations = getTranslations(user.locale);

        // Get title translation or default title message
        const title = translations[t('thread.header.thread')] || 'Thread';

        let subtitle = '';
        if (channel?.type === General.DM_CHANNEL) {
            subtitle = channel.displayName;
        } else {
            // Get translation or default message
            subtitle = translations[t('thread.header.thread_in')] || 'in {channelName}';
            subtitle = subtitle.replace('{channelName}', channel.displayName);
        }

        EphemeralStore.setCurrentThreadId(rootId);

        if (isFromNotification) {
            await dismissAllModalsAndPopToRoot();
            await NavigationStore.waitUntilScreenIsTop(Screens.HOME);
            if (switchingTeams && isTabletDevice) {
                DeviceEventEmitter.emit(Navigation.NAVIGATION_HOME, Screens.GLOBAL_THREADS);
            }
        }
        goToScreen(Screens.THREAD, '', {rootId}, {
            topBar: {
                title: {
                    text: title,
                },
                subtitle: {
                    color: changeOpacity(EphemeralStore.theme!.sidebarHeaderTextColor, 0.72),
                    text: subtitle,
                },
                noBorder: true,
                scrollEdgeAppearance: {
                    noBorder: true,
                },
                rightButtons,
            },
        });

        return {};
    } catch (error) {
        logError('Failed switchToThread', error);
        return {error};
    }
};

// When new post arrives:
// 1. If a reply, then update the reply_count, add user as the participant
// 2. Else add the post as a thread
export async function createThreadFromNewPost(serverUrl: string, post: Post, prepareRecordsOnly = false) {
    try {
        const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
        const models: Model[] = [];
        if (post.root_id) {
            // Update the thread data: `reply_count`
            const {model: threadModel} = await updateThread(serverUrl, post.root_id, {reply_count: post.reply_count}, true);
            if (threadModel) {
                models.push(threadModel);
            }

            // Add user as a participant to the thread
            const threadParticipantModels = await operator.handleThreadParticipants({
                threadsParticipants: [{
                    thread_id: post.root_id,
                    participants: [{
                        thread_id: post.root_id,
                        id: post.user_id,
                    }],
                }],
                prepareRecordsOnly: true,
                skipSync: true,
            });
            models.push(...threadParticipantModels);
        } else { // If the post is a root post, then we need to add it to the thread table
            const threadModels = await prepareThreadsFromReceivedPosts(operator, [post], false);
            models.push(...threadModels);
        }

        if (!prepareRecordsOnly) {
            await operator.batchRecords(models);
        }

        return {models};
    } catch (error) {
        logError('Failed createThreadFromNewPost', error);
        return {error};
    }
}

// On receiving threads, Along with the "threads" & "thread participants", extract and save "posts" & "users"
export async function processReceivedThreads(serverUrl: string, threads: Thread[], teamId: string, prepareRecordsOnly = false) {
    try {
        const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
        const currentUserId = await getCurrentUserId(database);

        const posts: Post[] = [];
        const users: UserProfile[] = [];
        const threadsToHandle: ThreadWithLastFetchedAt[] = [];

        // Extract posts & users from the received threads
        for (let i = 0; i < threads.length; i++) {
            const {participants, post} = threads[i];
            posts.push(post);
            participants.forEach((participant) => {
                if (currentUserId !== participant.id) {
                    users.push(participant);
                }
            });
            threadsToHandle.push({...threads[i], lastFetchedAt: post.create_at});
        }

        const postModels = await operator.handlePosts({
            actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
            order: [],
            posts,
            prepareRecordsOnly: true,
        });

        const threadModels = await operator.handleThreads({
            threads: threadsToHandle,
            teamId,
            prepareRecordsOnly: true,
        });

        const models = [...postModels, ...threadModels];

        if (users.length) {
            const userModels = await operator.handleUsers({
                users,
                prepareRecordsOnly: true,
            });
            models.push(...userModels);
        }

        if (!prepareRecordsOnly) {
            await operator.batchRecords(models);
        }
        return {models};
    } catch (error) {
        logError('Failed processReceivedThreads', error);
        return {error};
    }
}

export async function markTeamThreadsAsRead(serverUrl: string, teamId: string, prepareRecordsOnly = false) {
    try {
        const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
        const threads = await queryThreadsInTeam(database, teamId, true, true, true).fetch();
        const models = threads.map((thread) => thread.prepareUpdate((record) => {
            record.unreadMentions = 0;
            record.unreadReplies = 0;
            record.lastViewedAt = Date.now();
            record.viewedAt = Date.now();
        }));
        if (!prepareRecordsOnly) {
            await operator.batchRecords(models);
        }
        return {models};
    } catch (error) {
        logError('Failed markTeamThreadsAsRead', error);
        return {error};
    }
}

export async function markThreadAsViewed(serverUrl: string, threadId: string, prepareRecordsOnly = false) {
    try {
        const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
        const thread = await getThreadById(database, threadId);
        if (!thread) {
            return {error: 'Thread not found'};
        }

        thread.prepareUpdate((th) => {
            th.viewedAt = thread.lastViewedAt;
            th.lastViewedAt = Date.now();
        });

        if (!prepareRecordsOnly) {
            await operator.batchRecords([thread]);
        }

        return {model: thread};
    } catch (error) {
        logError('Failed markThreadAsViewed', error);
        return {error};
    }
}

export async function updateThread(serverUrl: string, threadId: string, updatedThread: Partial<ThreadWithViewedAt>, prepareRecordsOnly = false) {
    try {
        const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
        const thread = await getThreadById(database, threadId);
        if (!thread) {
            throw new Error('Thread not found');
        }

        const model = thread.prepareUpdate((record) => {
            record.isFollowing = updatedThread.is_following ?? record.isFollowing;
            record.replyCount = updatedThread.reply_count ?? record.replyCount;
            record.lastViewedAt = updatedThread.last_viewed_at ?? record.lastViewedAt;
            record.viewedAt = updatedThread.viewed_at ?? record.viewedAt;
            record.unreadMentions = updatedThread.unread_mentions ?? record.unreadMentions;
            record.unreadReplies = updatedThread.unread_replies ?? record.unreadReplies;
        });
        if (!prepareRecordsOnly) {
            await operator.batchRecords([model]);
        }
        return {model};
    } catch (error) {
        logError('Failed updateThread', error);
        return {error};
    }
}

export async function updateTeamThreadsSync(serverUrl: string, data: TeamThreadsSync, prepareRecordsOnly = false) {
    try {
        const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
        const models = await operator.handleTeamThreadsSync({data: [data], prepareRecordsOnly});
        if (!prepareRecordsOnly) {
            await operator.batchRecords(models);
        }
        return {models};
    } catch (error) {
        logError('Failed updateTeamThreadsSync', error);
        return {error};
    }
}
