import crypto from 'node:crypto';

import { getAffiliateEntityUrl } from './affiliates.js';
import { curateEntity } from './entities.js';
import { knexOwner as knex } from './knex.js';
import initLogger from './logger.js';
import redis from './redis.js';

const logger = initLogger();

function getCampaignUrl(campaign, entity) {
	if (!campaign) {
		return null;
	}

	if (campaign.url) {
		return campaign.url;
	}

	if (campaign.affiliate?.url) {
		return campaign.affiliate.url;
	}

	if (campaign.entity) {
		// resolve e.g. parameter tracking
		return getAffiliateEntityUrl(entity, campaign.affiliate);
	}

	return null;
}

function curateCampaign(campaign) {
	if (!campaign) {
		return null;
	}

	const entity = campaign.entity && curateEntity({ ...campaign.entity, parent: campaign.parent_entity });

	const curatedCampaign = {
		id: campaign.id,
		entity,
		banner: campaign.banner && {
			id: campaign.banner.id,
			type: campaign.banner.type,
			width: campaign.banner.width,
			height: campaign.banner.height,
			ratio: campaign.banner.ratio,
			entity: campaign.banner_entity && curateEntity({ ...campaign.banner_entity, parent: campaign.banner_parent_entity }),
			tags: campaign.banner_tags || [],
		},
		affiliate: campaign.affiliate && {
			id: campaign.affiliate.id,
			url: campaign.affiliate.url,
			parameters: campaign.affiliate.parameters,
		},
	};

	curatedCampaign.url = getCampaignUrl(campaign, entity);

	return curatedCampaign;
}

function selectRandomCampaign(primaryCampaigns, entityCampaigns, preferredCampaigns) {
	if (primaryCampaigns.length > 0) {
		return primaryCampaigns[crypto.randomInt(primaryCampaigns.length)];
	}

	if (entityCampaigns.length > 0) {
		return entityCampaigns[crypto.randomInt(entityCampaigns.length)];
	}

	if (preferredCampaigns.length > 0) {
		return preferredCampaigns[crypto.randomInt(preferredCampaigns.length)];
	}

	return null;
}

export async function getRandomCampaign(options = {}, context = {}, pass = 0) {
	const campaigns = options.campaigns
		|| await redis.hGetAll('traxxx:campaigns').then((rawCampaigns) => Object.values(rawCampaigns).map((rawCampaign) => JSON.parse(rawCampaign)));

	const validCampaigns = campaigns.filter((campaign) => {
		if (options.minRatio && (!campaign.banner || campaign.banner.ratio < options.minRatio)) {
			// too small
			return false;
		}

		if (options.maxRatio && (!campaign.banner || campaign.banner.ratio > options.maxRatio)) {
			// too big
			return false;
		}

		if (options.entityIds && !options.entityIds.some((entityId) => campaign.entity.id === entityId || campaign.entity.parent?.id === entityId)) {
			// this is an entity page, this campaign does not belong to this entity
			return false;
		}

		if (campaign.affiliate?.parameters?.global === false && !options.entityIds) {
			// this campaign should only show on entity page
			return false;
		}

		if (context.tagFilter && campaign.banner && campaign.banner.tags.some((tag) => context.tagFilter.includes(tag) && !options.tagSlugs?.includes(tag))) {
			// wrong tag
			return false;
		}

		// tag page overrides tag filter
		if (options.tagSlugs && campaign.banner && !campaign.banner.tags.some((tag) => options.tagSlugs.includes(tag))) {
			return false;
		}

		return true;
	});

	const campaignsByEntityId = validCampaigns.reduce((acc, campaign) => {
		const entityId = campaign.entity.parent?.id || campaign.entity.id;

		if (!acc[entityId]) {
			acc[entityId] = [];
		}

		acc[entityId].push(campaign);

		return acc;
	}, {});

	// randomize entities first to ensure fair exposure for entities with fewer banners
	const entityIds = Object.keys(campaignsByEntityId);
	const randomEntityCampaigns = entityIds.length > 0 ? campaignsByEntityId[entityIds[crypto.randomInt(entityIds.length)]] : [];
	const primaryCampaigns = randomEntityCampaigns.filter((campaign) => campaign.entity.id === options.entityIds?.[0]);

	const randomCampaign = selectRandomCampaign(primaryCampaigns, randomEntityCampaigns, validCampaigns, campaigns, options);

	// no campaign found, gradually widen scope
	if (!randomCampaign && pass === 0 && options.allowRandomFallback !== false) {
		return getRandomCampaign({
			minRatio: options.minRatio,
			maxRatio: options.maxRatio,
		}, context, pass + 1);
	}

	if (!randomCampaign && pass === 1 && options.allowRandomFallback !== false && options.allowRandomRatio) {
		return getRandomCampaign({}, context, pass + 1);
	}

	return randomCampaign;
}

export async function getRandomCampaigns(allOptions = [], context = {}) {
	const rawCampaigns = await redis.hGetAll('traxxx:campaigns');
	const campaigns = Object.values(rawCampaigns).map((rawCampaign) => JSON.parse(rawCampaign));

	return Promise.all(allOptions.map(async (options) => getRandomCampaign({
		...options,
		campaigns,
	}, context)));
}

export function getCampaignIndex(scenesCount) {
	return Math.floor((Math.random() * (0.5 - 0.2) + 0.2) * scenesCount); // avoid start and end of scenes list
}

export async function cacheCampaigns() {
	const campaigns = await knex('campaigns')
		.select('campaigns.*')
		.select(
			'campaigns.*',
			knex.raw('row_to_json(affiliates) as affiliate'),
			knex.raw('row_to_json(banners) as banner'),
			knex.raw('row_to_json(entities) as entity'),
			knex.raw('row_to_json(parents) as parent_entity'),
			knex.raw('row_to_json(banner_entities) as banner_entity'),
			knex.raw('row_to_json(banner_parents) as banner_parent_entity'),
			knex.raw('json_agg(tags.slug) filter (where tags.id is not null) as banner_tags'),
		)
		.leftJoin('affiliates', 'affiliates.id', 'campaigns.affiliate_id')
		.leftJoin('banners', 'banners.id', 'campaigns.banner_id')
		.leftJoin('banners_tags', 'banners_tags.banner_id', 'banners.id')
		.leftJoin('tags', 'tags.id', 'banners_tags.tag_id')
		.leftJoin('entities', 'entities.id', 'campaigns.entity_id')
		.leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
		.leftJoin('entities as banner_entities', 'banner_entities.id', 'banners.entity_id')
		.leftJoin('entities as banner_parents', 'banner_parents.id', 'banner_entities.parent_id')
		.groupBy(
			'campaigns.id',
			'affiliates.id',
			'entities.id',
			'parents.id',
			'banners.id',
			'banner_entities.id',
			'banner_parents.id',
		);

	await redis.del('traxxx:campaigns');

	await Promise.all(campaigns.map(async (campaign) => {
		const curatedCampaign = curateCampaign(campaign);

		await redis.hSet('traxxx:campaigns', campaign.id, JSON.stringify(curatedCampaign));
	}));

	logger.info('Cached campaigns');
}
