import { ResultAsync, errAsync, okAsync } from "neverthrow";
import { FlowExecCtx } from "@core/flow.execution.context";
import { Database, eq } from "@pkg/db";
import { link } from "@pkg/db/schema";
import { type Err } from "@pkg/result";
import { logger } from "@pkg/logger";
import { traceResultAsync } from "@core/observability";
import { CreateLink, Link, LinkWithDevice, UpdateLink } from "./data";
import { linkErrors } from "./errors";

export class LinkRepository {
    constructor(private db: Database) {}

    list(fctx: FlowExecCtx): ResultAsync<LinkWithDevice[], Err> {
        return traceResultAsync({
            name: "link.list",
            fctx,
            fn: () =>
                ResultAsync.fromPromise(
                    this.db.query.link.findMany({
                        orderBy: (link, { asc }) => [asc(link.createdAt)],
                        with: {
                            device: true,
                            supportedApp: true,
                        },
                    }),
                    (e) =>
                        linkErrors.listFailed(
                            fctx,
                            e instanceof Error ? e.message : String(e),
                        ),
                ).map((rows) => rows as LinkWithDevice[]),
        });
    }

    getById(fctx: FlowExecCtx, id: number): ResultAsync<LinkWithDevice, Err> {
        return traceResultAsync({
            name: "link.getById",
            fctx,
            fn: () =>
                ResultAsync.fromPromise(
                    this.db.query.link.findFirst({
                        where: eq(link.id, id),
                        with: {
                            device: true,
                            supportedApp: true,
                        },
                    }),
                    (e) =>
                        linkErrors.dbError(
                            fctx,
                            e instanceof Error ? e.message : String(e),
                        ),
                ).andThen((row) => {
                    if (!row) return errAsync(linkErrors.linkNotFound(fctx, id));
                    return okAsync(row as LinkWithDevice);
                }),
        });
    }

    getByToken(fctx: FlowExecCtx, token: string): ResultAsync<LinkWithDevice, Err> {
        return traceResultAsync({
            name: "link.getByToken",
            fctx,
            fn: () =>
                ResultAsync.fromPromise(
                    this.db.query.link.findFirst({
                        where: eq(link.token, token),
                        with: { device: true, supportedApp: true },
                    }),
                    (e) =>
                        linkErrors.dbError(
                            fctx,
                            e instanceof Error ? e.message : String(e),
                        ),
                ).andThen((row) => {
                    if (!row) return errAsync(linkErrors.linkNotFound(fctx, token));
                    return okAsync(row as LinkWithDevice);
                }),
        });
    }

    create(fctx: FlowExecCtx, data: CreateLink): ResultAsync<Link, Err> {
        logger.info("Creating link", { ...fctx, token: data.token });

        return traceResultAsync({
            name: "link.create",
            fctx,
            fn: () =>
                ResultAsync.fromPromise(
                    this.db
                        .insert(link)
                        .values({
                            token: data.token,
                            status: "active",
                            linkedDeviceId: data.linkedDeviceId ?? null,
                            supportedAppId: data.supportedAppId,
                            expiresAt: data.expiresAt ?? null,
                            createdAt: new Date(),
                            updatedAt: new Date(),
                        })
                        .returning()
                        .execute(),
                    (e) =>
                        linkErrors.createFailed(
                            fctx,
                            e instanceof Error ? e.message : String(e),
                        ),
                ).map((rows) => rows[0] as Link),
        });
    }

    update(
        fctx: FlowExecCtx,
        id: number,
        updates: UpdateLink,
    ): ResultAsync<Link, Err> {
        return traceResultAsync({
            name: "link.update",
            fctx,
            fn: () =>
                this.getById(fctx, id).andThen(() =>
                    ResultAsync.fromPromise(
                        this.db
                            .update(link)
                            .set({ ...updates, updatedAt: new Date() })
                            .where(eq(link.id, id))
                            .returning()
                            .execute(),
                        (e) =>
                            linkErrors.updateFailed(
                                fctx,
                                e instanceof Error ? e.message : String(e),
                            ),
                    ).andThen((rows) => {
                        if (!rows[0])
                            return errAsync(linkErrors.linkNotFound(fctx, id));
                        return okAsync(rows[0] as Link);
                    }),
                ),
        });
    }

    delete(fctx: FlowExecCtx, id: number): ResultAsync<boolean, Err> {
        return traceResultAsync({
            name: "link.delete",
            fctx,
            fn: () =>
                this.getById(fctx, id).andThen(() =>
                    ResultAsync.fromPromise(
                        this.db.delete(link).where(eq(link.id, id)).execute(),
                        (e) =>
                            linkErrors.deleteFailed(
                                fctx,
                                e instanceof Error ? e.message : String(e),
                            ),
                    ).map(() => true),
                ),
        });
    }

    touch(fctx: FlowExecCtx, token: string): ResultAsync<Link, Err> {
        return traceResultAsync({
            name: "link.touch",
            fctx,
            fn: () =>
                ResultAsync.fromPromise(
                    this.db
                        .update(link)
                        .set({ lastAccessedAt: new Date(), updatedAt: new Date() })
                        .where(eq(link.token, token))
                        .returning()
                        .execute(),
                    (e) =>
                        linkErrors.updateFailed(
                            fctx,
                            e instanceof Error ? e.message : String(e),
                        ),
                ).andThen((rows) => {
                    if (!rows[0])
                        return errAsync(linkErrors.linkNotFound(fctx, token));
                    return okAsync(rows[0] as Link);
                }),
        });
    }
}
