const db = require('../db/models');

const { Op } = db.Sequelize;

const EVENT_STATUS = {
  QUEUED: 'queued',
  RUNNING: 'running',
  SUCCESS: 'success',
  FAILED: 'failed',
};

const ENVIRONMENT = {
  STAGE: 'stage',
  PRODUCTION: 'production',
};

const sanitizeRecordForClone = (modelInstance) => {
  const data = modelInstance.toJSON();
  delete data.id;
  delete data.createdAt;
  delete data.updatedAt;
  delete data.deletedAt;
  delete data.deletedBy;
  delete data.importHash;
  return data;
};

module.exports = class PublishService {
  static async withProjectPublishLock(projectId, callback) {
    return db.sequelize.transaction(async (transaction) => {
      const project = await db.projects.findByPk(projectId, {
        transaction,
        lock: transaction.LOCK.UPDATE,
      });

      if (!project) {
        const error = new Error('Project not found');
        error.code = 404;
        throw error;
      }

      const runningEvent = await db.publish_events.findOne({
        where: {
          projectId,
          status: EVENT_STATUS.RUNNING,
        },
        order: [['createdAt', 'DESC']],
        transaction,
        lock: transaction.LOCK.UPDATE,
      });

      if (runningEvent) {
        const error = new Error('Publish is already running for this project');
        error.code = 400;
        throw error;
      }

      return callback(transaction);
    });
  }

  static async publishToProduction(projectId, currentUser, title, description) {
    if (!projectId) {
      const error = new Error('projectId is required');
      error.code = 400;
      throw error;
    }

    const eventTitle = typeof title === 'string' ? title.trim() : '';
    const eventDescription = typeof description === 'string' ? description.trim() : '';

    if (!eventTitle) {
      const error = new Error('title is required');
      error.code = 400;
      throw error;
    }

    if (!eventDescription) {
      const error = new Error('description is required');
      error.code = 400;
      throw error;
    }

    const publishEvent = await db.publish_events.create({
      projectId,
      userId: currentUser?.id || null,
      title: eventTitle,
      description: eventDescription,
      from_environment: ENVIRONMENT.STAGE,
      to_environment: ENVIRONMENT.PRODUCTION,
      status: EVENT_STATUS.QUEUED,
      createdById: currentUser?.id || null,
      updatedById: currentUser?.id || null,
    });

    try {
      const summary = await this.withProjectPublishLock(projectId, async (transaction) => {
        await publishEvent.update(
          {
            started_at: new Date(),
            status: EVENT_STATUS.RUNNING,
            error_message: null,
            updatedById: currentUser?.id || null,
          },
          { transaction },
        );

        return this.copyStageToProduction(projectId, currentUser, transaction);
      });

      await publishEvent.update({
        status: EVENT_STATUS.SUCCESS,
        finished_at: new Date(),
        pages_copied: summary.pages_copied,
        transitions_copied: summary.transitions_copied,
        audios_copied: summary.audios_copied,
        error_message: null,
        updatedById: currentUser?.id || null,
      });

      return {
        success: true,
        publishEventId: publishEvent.id,
        summary,
      };
    } catch (error) {
      await publishEvent.update({
        status: EVENT_STATUS.FAILED,
        finished_at: new Date(),
        error_message: error.message,
        updatedById: currentUser?.id || null,
      });
      throw error;
    }
  }

  static async copyStageToProduction(projectId, currentUser, transaction) {
      const [stagePages, stageTransitions, stageAudioTracks] = await Promise.all([
        db.tour_pages.findAll({
          where: { projectId, environment: ENVIRONMENT.STAGE },
          transaction,
        }),
        db.transitions.findAll({
          where: { projectId, environment: ENVIRONMENT.STAGE },
          transaction,
        }),
        db.project_audio_tracks.findAll({
          where: { projectId, environment: ENVIRONMENT.STAGE },
          transaction,
        }),
      ]);

      const stagePageIds = stagePages.map((page) => page.id);
      const stageTransitionIds = stageTransitions.map((transition) => transition.id);

      const [stageElements, stageLinks] = await Promise.all([
        stagePageIds.length
          ? db.page_elements.findAll({
              where: { pageId: { [Op.in]: stagePageIds } },
              transaction,
            })
          : [],
        stagePageIds.length || stageTransitionIds.length
          ? db.page_links.findAll({
              where: {
                [Op.or]: [
                  stagePageIds.length ? { from_pageId: { [Op.in]: stagePageIds } } : null,
                  stagePageIds.length ? { to_pageId: { [Op.in]: stagePageIds } } : null,
                  stageTransitionIds.length ? { transitionId: { [Op.in]: stageTransitionIds } } : null,
                ].filter(Boolean),
              },
              transaction,
            })
          : [],
      ]);

      const productionPages = await db.tour_pages.findAll({
        where: { projectId, environment: ENVIRONMENT.PRODUCTION },
        attributes: ['id'],
        transaction,
      });
      const productionTransitions = await db.transitions.findAll({
        where: { projectId, environment: ENVIRONMENT.PRODUCTION },
        attributes: ['id'],
        transaction,
      });

      const productionPageIds = productionPages.map((page) => page.id);
      const productionTransitionIds = productionTransitions.map((transition) => transition.id);

      if (productionPageIds.length || productionTransitionIds.length) {
        await db.page_links.destroy({
          where: {
            [Op.or]: [
              productionPageIds.length ? { from_pageId: { [Op.in]: productionPageIds } } : null,
              productionPageIds.length ? { to_pageId: { [Op.in]: productionPageIds } } : null,
              productionTransitionIds.length ? { transitionId: { [Op.in]: productionTransitionIds } } : null,
            ].filter(Boolean),
          },
          transaction,
        });
      }

      if (productionPageIds.length) {
        await db.page_elements.destroy({
          where: { pageId: { [Op.in]: productionPageIds } },
          transaction,
        });
      }

      await Promise.all([
        db.tour_pages.destroy({
          where: { projectId, environment: ENVIRONMENT.PRODUCTION },
          transaction,
        }),
        db.transitions.destroy({
          where: { projectId, environment: ENVIRONMENT.PRODUCTION },
          transaction,
        }),
        db.project_audio_tracks.destroy({
          where: { projectId, environment: ENVIRONMENT.PRODUCTION },
          transaction,
        }),
      ]);

      const actorId = currentUser?.id || null;

      const productionPagesPayload = stagePages.map((stagePage) => {
        const data = sanitizeRecordForClone(stagePage);
        return {
          ...data,
          projectId,
          environment: ENVIRONMENT.PRODUCTION,
          source_key: stagePage.id,
          createdById: actorId,
          updatedById: actorId,
        };
      });

      const productionTransitionsPayload = stageTransitions.map((stageTransition) => {
        const data = sanitizeRecordForClone(stageTransition);
        return {
          ...data,
          projectId,
          environment: ENVIRONMENT.PRODUCTION,
          source_key: stageTransition.id,
          createdById: actorId,
          updatedById: actorId,
        };
      });

      const productionAudioPayload = stageAudioTracks.map((stageAudio) => {
        const data = sanitizeRecordForClone(stageAudio);
        return {
          ...data,
          projectId,
          environment: ENVIRONMENT.PRODUCTION,
          source_key: stageAudio.id,
          createdById: actorId,
          updatedById: actorId,
        };
      });

      const [createdPages, createdTransitions] = await Promise.all([
        productionPagesPayload.length
          ? db.tour_pages.bulkCreate(productionPagesPayload, { transaction, returning: true })
          : [],
        productionTransitionsPayload.length
          ? db.transitions.bulkCreate(productionTransitionsPayload, { transaction, returning: true })
          : [],
      ]);

      if (productionAudioPayload.length) {
        await db.project_audio_tracks.bulkCreate(productionAudioPayload, {
          transaction,
          returning: false,
        });
      }

      const pageMap = new Map(createdPages.map((page) => [page.source_key, page.id]));
      const transitionMap = new Map(createdTransitions.map((transition) => [transition.source_key, transition.id]));

      const productionElementsPayload = stageElements.reduce((acc, stageElement) => {
        const mappedPageId = pageMap.get(stageElement.pageId);
        if (!mappedPageId) {
          return acc;
        }

        const data = sanitizeRecordForClone(stageElement);
        acc.push({
          ...data,
          pageId: mappedPageId,
          createdById: actorId,
          updatedById: actorId,
        });
        return acc;
      }, []);

      if (productionElementsPayload.length) {
        await db.page_elements.bulkCreate(productionElementsPayload, {
          transaction,
          returning: false,
        });
      }

      const productionLinksPayload = stageLinks.reduce((acc, stageLink) => {
        const mappedFromPageId = stageLink.from_pageId ? pageMap.get(stageLink.from_pageId) : null;
        if (stageLink.from_pageId && !mappedFromPageId) {
          return acc;
        }

        const data = sanitizeRecordForClone(stageLink);
        acc.push({
          ...data,
          from_pageId: mappedFromPageId || null,
          to_pageId: stageLink.to_pageId ? pageMap.get(stageLink.to_pageId) || null : null,
          transitionId: stageLink.transitionId
            ? transitionMap.get(stageLink.transitionId) || null
            : null,
          createdById: actorId,
          updatedById: actorId,
        });
        return acc;
      }, []);

      if (productionLinksPayload.length) {
        await db.page_links.bulkCreate(productionLinksPayload, {
          transaction,
          returning: false,
        });
      }

      return {
        pages_copied: stagePages.length,
        transitions_copied: stageTransitions.length,
        audios_copied: stageAudioTracks.length,
        elements_copied: productionElementsPayload.length,
        links_copied: productionLinksPayload.length,
      };
  }
};
