import type { Prisma, PrismaClient, Station, Tag } from "@prisma/client";
import { prisma } from "~/db.server";
import { upsertTagOnName } from "~/models/tag.server";
import type { ConvertDatesToStrings } from "~/utils";
import { slugify } from "~/utils";

export type StationInput = {
    id?: string
    name: string;
    description: string;
    streamUrl: string;
    imgUrl: string;
    reliability: number;
    popularity: number;
    tags: string[]
};

export type PrismaTxClient = Omit<PrismaClient, "$connect" | "$disconnect" | "$on" | "$transaction" | "$use">;

/**
 * Fetch stations with a reliability score GTE to the `reliability` parameter.
 */
export function getStations(reliability: number = 80) {
    return prisma.station.findMany({
        where: { reliability: { gte: reliability } },
        include: {
            tags: {
                include: {
                    tag: true
                }
            }
        },
        orderBy: [
            { reliability: "desc" },
            { popularity: "desc" }
        ]
    });
}

/**
 * 
 * @param stationId current station id that we want to get next and prev
 * @returns 
 */
export const getPrevNextStations = async (stationId?: string) => {
    const defaultData = {
        prev: undefined,
        next: undefined
    }
    if (!stationId) return defaultData;

    const stations = await getStations();

    return stations.reduce((prev, currStation, index, array) => {
        if (currStation.id === stationId) {
            return {
                prev: array[index - 1]?.id,
                next: array[index + 1]?.id,
            }
        }
        return prev
    }, defaultData as {
        prev?: string,
        next?: string,
    })
}

/**
 * Fetch stations tagged with `tags` and a reliability score GTE to the `reliability` parameter.
 */
export function findStationsByTags(tags: string[], q: string | null, reliability: number = 80) {
    const nameCondition = q ? {
        contains: q
    } : {};
    return prisma.station.findMany({
        where: {
            reliability: { gte: reliability },
            name: nameCondition,
            tags: {
                some: {
                    tag: {
                        name: { in: tags }
                    }
                }
            }
        },
        include: {
            tags: {
                include: {
                    tag: true
                }
            }
        },
        orderBy: [
            { reliability: "desc" },
            { popularity: "desc" }
        ]
    });
}

export type StationWithTags = NonNullable<Prisma.PromiseReturnType<typeof getStationById>>;
export type StationWithTagsClientSide = ConvertDatesToStrings<StationWithTags>;

export function getStationById(id: string) {
    return prisma.station.findUnique({
        where: { id },
        include: {
            tags: {
                include: {
                    tag: true
                }
            }
        }
    });

}

/**
 * Performs upsert at the DB level with `ON CONFLICT`.  This is much faster than application level checks, which would require
 * several queries.
 * Note: certain criteria must be met to perform at the DB level.
 * For details, see https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#database-upsert-query-criteria
 */
export function upsertStationOnStreamUrl(input: StationInput, p: PrismaTxClient = prisma) {
    return p.station.upsert({
        // only one unique field in the upsert's where option
        where: { streamUrl: input.streamUrl },
        create: {
            name: input.name,
            slug: slugify(input.name),
            description: input.description,
            imgUrl: input.imgUrl,
            // unique field in the where option and the unique field in the create option have the same value
            streamUrl: input.streamUrl,
            reliability: input.reliability,
            popularity: input.popularity
        },
        update: {
            name: input.name,
            slug: slugify(input.name),
            description: input.description,
            imgUrl: input.imgUrl,
            reliability: input.reliability,
            popularity: input.popularity
        }
    });
}

export function deleteStationTags(station: Station, p: PrismaTxClient = prisma) {
    return p.stationTag.deleteMany({ where: { stationId: station.id } });
}

export function createStationTag(station: Station, tag: Tag, p: PrismaTxClient = prisma) {
    return p.stationTag.create({
        data: {
            tagId: tag.id,
            stationId: station.id
        }
    });
}

/**
 * Import the stations in `stationsInput` by either inserting or updating the records.
 * - stations are upserted with uniqueness constraint on `streamUrl`
 * - tags are upserted with uniqueness constraint on `name`
 * - station tags are set by removing and then adding tags from each `stationInput`.
 * - we may need to revisit this approach on station tags for performance and functional
 *   requirements
 * - all writes are performed in a single transaction for performance and correctness.
 */
export async function importStations(stationsInput: StationInput[]) {
    return prisma.$transaction(async (tx) => {
        const stations: Station[] = [];
        for (const stationInput of stationsInput) {
            const station = await upsertStationOnStreamUrl(stationInput, tx);
            await deleteStationTags(station, tx);
            for (const tagName of stationInput.tags) {
                const tag = await upsertTagOnName(tagName, tx);
                await createStationTag(station, tag, tx);
            }
            stations.push(station);
        }
        return stations;
    });


}
