'use strict';

const { Op } = require('sequelize');
const moment = require('moment-timezone');
const { Task, Tag, Project, sequelize } = require('../../models');
const searchRepository = require('./repository');
const { parseSearchParams, priorityToInt } = require('./validation');
const { serializeTasks } = require('../tasks/core/serializers');
const { UnauthorizedError } = require('../../shared/errors');

class SearchService {
    /**
     * Build date range condition for due/defer filters.
     */
    buildDateCondition(filterValue, startOfToday, fieldName) {
        if (!filterValue) return null;

        let startDate, endDate;

        switch (filterValue) {
            case 'today':
                startDate = startOfToday.clone();
                endDate = startOfToday.clone().endOf('day');
                break;
            case 'tomorrow':
                startDate = startOfToday.clone().add(1, 'day');
                endDate = startOfToday.clone().add(1, 'day').endOf('day');
                break;
            case 'next_week':
                startDate = startOfToday.clone();
                endDate = startOfToday.clone().add(7, 'days').endOf('day');
                break;
            case 'next_month':
                startDate = startOfToday.clone();
                endDate = startOfToday.clone().add(1, 'month').endOf('day');
                break;
            default:
                return null;
        }

        return {
            [fieldName]: {
                [Op.between]: [startDate.toDate(), endDate.toDate()],
            },
        };
    }

    /**
     * Build task search conditions.
     */
    buildTaskConditions(
        userId,
        params,
        dueDateCondition,
        deferDateCondition,
        nowDate
    ) {
        const { searchQuery, priority, recurring, extras, excludeSubtasks } =
            params;

        const conditions = { user_id: userId };
        const extraConditions = [];

        if (excludeSubtasks) {
            conditions.parent_task_id = null;
            conditions.recurring_parent_id = null;
        }

        if (searchQuery) {
            const lowerQuery = searchQuery.toLowerCase();
            conditions[Op.or] = [
                sequelize.where(
                    sequelize.fn('LOWER', sequelize.col('Task.name')),
                    {
                        [Op.like]: `%${lowerQuery}%`,
                    }
                ),
                sequelize.where(
                    sequelize.fn('LOWER', sequelize.col('Task.note')),
                    {
                        [Op.like]: `%${lowerQuery}%`,
                    }
                ),
            ];
        }

        if (priority) {
            const priorityInt = priorityToInt(priority);
            if (priorityInt !== null) {
                conditions.priority = priorityInt;
            }
        }

        if (dueDateCondition) {
            extraConditions.push(dueDateCondition);
        }

        if (deferDateCondition) {
            extraConditions.push(deferDateCondition);
        }

        if (recurring) {
            switch (recurring) {
                case 'recurring':
                    conditions.recurrence_type = { [Op.ne]: 'none' };
                    conditions.recurring_parent_id = null;
                    break;
                case 'non_recurring':
                    conditions[Op.or] = [
                        { recurrence_type: 'none' },
                        { recurrence_type: null },
                    ];
                    conditions.recurring_parent_id = null;
                    break;
                case 'instances':
                    conditions.recurring_parent_id = { [Op.ne]: null };
                    break;
            }
        }

        if (extras.has('recurring')) {
            extraConditions.push({
                [Op.or]: [
                    { recurrence_type: { [Op.ne]: 'none' } },
                    { recurring_parent_id: { [Op.ne]: null } },
                ],
            });
        }

        if (extras.has('overdue')) {
            extraConditions.push({ due_date: { [Op.lt]: nowDate } });
            extraConditions.push({ completed_at: null });
        }

        if (extras.has('has_content')) {
            extraConditions.push(
                sequelize.where(
                    sequelize.fn(
                        'LENGTH',
                        sequelize.fn('TRIM', sequelize.col('Task.note'))
                    ),
                    { [Op.gt]: 0 }
                )
            );
        }

        if (extras.has('deferred')) {
            extraConditions.push({ defer_until: { [Op.gt]: nowDate } });
        }

        if (extras.has('assigned_to_project')) {
            extraConditions.push({ project_id: { [Op.ne]: null } });
        }

        if (extraConditions.length > 0) {
            conditions[Op.and] = extraConditions;
        }

        return conditions;
    }

    /**
     * Build task include config.
     */
    buildTaskInclude(tagIds, extras) {
        const include = [
            { model: Project, attributes: ['id', 'uid', 'name'] },
            {
                model: Task,
                as: 'Subtasks',
                include: [
                    {
                        model: Tag,
                        attributes: ['id', 'name', 'uid'],
                        through: { attributes: [] },
                    },
                ],
            },
        ];

        const requireTags = tagIds.length > 0 || extras.has('has_tags');
        const tagInclude = {
            model: Tag,
            through: { attributes: [] },
            attributes: ['id', 'name', 'uid'],
            required: requireTags,
        };

        if (tagIds.length > 0) {
            tagInclude.where = { id: { [Op.in]: tagIds } };
        }

        include.push(tagInclude);
        return { include, tagInclude: requireTags ? tagInclude : undefined };
    }

    /**
     * Search tasks.
     */
    async searchTasks(
        userId,
        params,
        tagIds,
        dueDateCondition,
        deferDateCondition,
        nowDate,
        timezone
    ) {
        const conditions = this.buildTaskConditions(
            userId,
            params,
            dueDateCondition,
            deferDateCondition,
            nowDate
        );
        const { include, tagInclude } = this.buildTaskInclude(
            tagIds,
            params.extras
        );

        let count = 0;
        if (params.hasPagination) {
            count = await searchRepository.countTasks(
                conditions,
                tagInclude ? [tagInclude] : undefined
            );
        }

        const tasks = await searchRepository.findTasks(
            conditions,
            include,
            params.limit,
            params.offset
        );

        const serializedTasks = await serializeTasks(tasks, timezone, {
            skipDisplayNameTransform: true,
        });

        return {
            count,
            results: serializedTasks.map((task) => ({
                type: 'Task',
                ...task,
                description: task.note,
            })),
        };
    }

    /**
     * Search projects.
     */
    async searchProjects(userId, params, tagIds, dueDateCondition) {
        const { searchQuery, priority, extras, hasPagination, limit, offset } =
            params;

        const conditions = { user_id: userId };

        if (searchQuery) {
            const lowerQuery = searchQuery.toLowerCase();
            conditions[Op.or] = [
                sequelize.where(
                    sequelize.fn('LOWER', sequelize.col('Project.name')),
                    {
                        [Op.like]: `%${lowerQuery}%`,
                    }
                ),
                sequelize.where(
                    sequelize.fn('LOWER', sequelize.col('Project.description')),
                    {
                        [Op.like]: `%${lowerQuery}%`,
                    }
                ),
            ];
        }

        if (priority) {
            conditions.priority = priority;
        }

        if (dueDateCondition) {
            conditions.due_date_at = dueDateCondition.due_date;
        }

        const requireTags = tagIds.length > 0 || extras.has('has_tags');
        const include = [];

        if (requireTags) {
            const tagInclude = {
                model: Tag,
                through: { attributes: [] },
                attributes: [],
                required: true,
            };
            if (tagIds.length > 0) {
                tagInclude.where = { id: { [Op.in]: tagIds } };
            }
            include.push(tagInclude);
        }

        let count = 0;
        if (hasPagination) {
            count = await searchRepository.countProjects(conditions, include);
        }

        const projects = await searchRepository.findProjects(
            conditions,
            include,
            limit,
            offset
        );

        return {
            count,
            results: projects.map((project) => ({
                type: 'Project',
                id: project.id,
                uid: project.uid,
                name: project.name,
                description: project.description,
                priority: project.priority,
                status: project.status,
            })),
        };
    }

    /**
     * Search areas.
     */
    async searchAreas(userId, params) {
        const { searchQuery, hasPagination, limit, offset } = params;

        const conditions = { user_id: userId };

        if (searchQuery) {
            const lowerQuery = searchQuery.toLowerCase();
            conditions[Op.or] = [
                sequelize.where(
                    sequelize.fn('LOWER', sequelize.col('Area.name')),
                    {
                        [Op.like]: `%${lowerQuery}%`,
                    }
                ),
                sequelize.where(
                    sequelize.fn('LOWER', sequelize.col('Area.description')),
                    {
                        [Op.like]: `%${lowerQuery}%`,
                    }
                ),
            ];
        }

        let count = 0;
        if (hasPagination) {
            count = await searchRepository.countAreas(conditions);
        }

        const areas = await searchRepository.findAreas(
            conditions,
            limit,
            offset
        );

        return {
            count,
            results: areas.map((area) => ({
                type: 'Area',
                id: area.id,
                uid: area.uid,
                name: area.name,
                description: area.description,
            })),
        };
    }

    /**
     * Search notes.
     */
    async searchNotes(userId, params, tagIds) {
        const { searchQuery, hasPagination, limit, offset } = params;

        const conditions = { user_id: userId };

        if (searchQuery) {
            const lowerQuery = searchQuery.toLowerCase();
            conditions[Op.or] = [
                sequelize.where(
                    sequelize.fn('LOWER', sequelize.col('Note.title')),
                    {
                        [Op.like]: `%${lowerQuery}%`,
                    }
                ),
                sequelize.where(
                    sequelize.fn('LOWER', sequelize.col('Note.content')),
                    {
                        [Op.like]: `%${lowerQuery}%`,
                    }
                ),
            ];
        }

        const include = [];
        if (tagIds.length > 0) {
            include.push({
                model: Tag,
                where: { id: { [Op.in]: tagIds } },
                through: { attributes: [] },
                attributes: [],
                required: true,
            });
        }

        let count = 0;
        if (hasPagination) {
            count = await searchRepository.countNotes(conditions, include);
        }

        const notes = await searchRepository.findNotes(
            conditions,
            include,
            limit,
            offset
        );

        return {
            count,
            results: notes.map((note) => ({
                type: 'Note',
                id: note.id,
                uid: note.uid,
                name: note.title,
                title: note.title,
                description: note.content ? note.content.substring(0, 100) : '',
            })),
        };
    }

    /**
     * Search tags.
     */
    async searchTags(userId, params) {
        const { searchQuery, hasPagination, limit, offset } = params;

        const conditions = { user_id: userId };

        if (searchQuery) {
            const lowerQuery = searchQuery.toLowerCase();
            conditions[Op.and] = [
                sequelize.where(
                    sequelize.fn('LOWER', sequelize.col('Tag.name')),
                    {
                        [Op.like]: `%${lowerQuery}%`,
                    }
                ),
            ];
        }

        let count = 0;
        if (hasPagination) {
            count = await searchRepository.countTags(conditions);
        }

        const tags = await searchRepository.findTags(conditions, limit, offset);

        return {
            count,
            results: tags.map((tag) => ({
                type: 'Tag',
                id: tag.id,
                uid: tag.uid,
                name: tag.name,
            })),
        };
    }

    /**
     * Universal search across all entity types.
     */
    async search(userId, query, timezone = 'UTC') {
        if (!userId) {
            throw new UnauthorizedError('Unauthorized');
        }

        const params = parseSearchParams(query);
        const {
            filterTypes,
            tagNames,
            due,
            defer,
            hasPagination,
            limit,
            offset,
        } = params;

        // Find tag IDs if filtering by tags
        const tagIds = await searchRepository.findTagIdsByNames(
            userId,
            tagNames
        );
        if (tagNames.length > 0 && tagIds.length === 0) {
            return { results: [] };
        }

        // Calculate date conditions
        const nowMoment = moment().tz(timezone);
        const startOfToday = nowMoment.clone().startOf('day');
        const nowDate = nowMoment.toDate();

        const dueDateCondition = this.buildDateCondition(
            due,
            startOfToday,
            'due_date'
        );
        const deferDateCondition = this.buildDateCondition(
            defer,
            startOfToday,
            'defer_until'
        );

        const results = [];
        let totalCount = 0;

        // Search each entity type
        if (filterTypes.includes('Task')) {
            const taskResults = await this.searchTasks(
                userId,
                params,
                tagIds,
                dueDateCondition,
                deferDateCondition,
                nowDate,
                timezone
            );
            results.push(...taskResults.results);
            totalCount += taskResults.count;
        }

        if (filterTypes.includes('Project')) {
            const projectResults = await this.searchProjects(
                userId,
                params,
                tagIds,
                dueDateCondition
            );
            results.push(...projectResults.results);
            totalCount += projectResults.count;
        }

        if (filterTypes.includes('Area')) {
            const areaResults = await this.searchAreas(userId, params);
            results.push(...areaResults.results);
            totalCount += areaResults.count;
        }

        if (filterTypes.includes('Note')) {
            const noteResults = await this.searchNotes(userId, params, tagIds);
            results.push(...noteResults.results);
            totalCount += noteResults.count;
        }

        if (filterTypes.includes('Tag')) {
            const tagResults = await this.searchTags(userId, params);
            results.push(...tagResults.results);
            totalCount += tagResults.count;
        }

        if (hasPagination) {
            return {
                results,
                pagination: {
                    total: totalCount,
                    limit,
                    offset,
                    hasMore: offset + results.length < totalCount,
                },
            };
        }

        return { results };
    }
}

module.exports = new SearchService();
