import { z } from "zod";
import { db } from "../db.js";
import { Label, logger } from "../logger.js";
import { assembleUrl, updateCapsForIndexer } from "../torznab.js";
import { USER_AGENT } from "../constants.js";
import { getAllIndexers } from "../indexers.js";
import { resultOf, resultOfErr } from "../Result.js";
import ms from "ms";
// Helper function to deserialize raw database row (snake_case columns) to Indexer
function deserializeRawRow(rawRow) {
    const row = rawRow;
    return {
        id: row.id,
        name: row.name,
        url: row.url,
        apikey: row.apikey,
        enabled: Boolean(row.enabled),
        status: row.status,
        retryAfter: row.retry_after,
        searchCap: Boolean(row.search_cap),
        tvSearchCap: Boolean(row.tv_search_cap),
        movieSearchCap: Boolean(row.movie_search_cap),
        musicSearchCap: Boolean(row.music_search_cap),
        audioSearchCap: Boolean(row.audio_search_cap),
        bookSearchCap: Boolean(row.book_search_cap),
        tvIdCaps: row.tv_id_caps ? JSON.parse(row.tv_id_caps) : null,
        movieIdCaps: row.movie_id_caps
            ? JSON.parse(row.movie_id_caps)
            : null,
        categories: row.cat_caps ? JSON.parse(row.cat_caps) : null,
        limits: row.limits_caps ? JSON.parse(row.limits_caps) : null,
    };
}
// Validation schemas
export const indexerCreateSchema = z.object({
    name: z.string().min(1).optional(),
    url: z.string().url(),
    apikey: z.string().min(1),
    enabled: z.boolean().default(true),
});
export const indexerUpdateSchema = z.object({
    id: z.number().int().positive(),
    name: z.string().min(1).optional().nullable(),
    url: z.string().url().optional(),
    apikey: z.string().min(1).optional(),
    enabled: z.boolean().optional(),
});
export const indexerTestSchema = z.object({
    url: z.string().url(),
    apikey: z.string().min(1),
});
export async function testIndexerConnection(url, apikey, name) {
    try {
        const response = await fetch(assembleUrl(url, apikey, { t: "caps" }), {
            headers: { "User-Agent": USER_AGENT },
            signal: AbortSignal.timeout(ms("5 seconds")),
        });
        if (!response.ok) {
            if (response.status === 401) {
                return resultOfErr({
                    code: "AUTH_FAILED",
                    message: "Authentication failed - check API key",
                });
            }
            else if (response.status === 429) {
                return resultOfErr({
                    code: "RATE_LIMITED",
                    message: "Rate limited by indexer",
                });
            }
            else {
                return resultOfErr({
                    code: "CONNECTION_FAILED",
                    message: "Connection failed",
                });
            }
        }
        logger.verbose({
            label: Label.TORZNAB,
            message: `Test connection successful for: ${name}`,
        });
        return resultOf({
            success: true,
            message: "Connection successful",
        });
    }
    catch (error) {
        const message = error.message;
        logger.warn({
            label: Label.TORZNAB,
            message: `Test connection failed for ${name}: ${message}`,
        });
        // Handle timeout specifically - AbortSignal.timeout() throws TimeoutError (DOMException)
        if (error.name === "TimeoutError" || error.name === "AbortError") {
            return resultOfErr({
                code: "TIMEOUT",
                message: "Connection timed out",
            });
        }
        return resultOfErr({
            code: "CONNECTION_FAILED",
            message: "Connection failed",
        });
    }
}
export async function createIndexer(input) {
    // Create new indexer only (no upsert)
    const [result] = await db("indexer")
        .insert({
        name: input.name || null,
        url: input.url,
        apikey: input.apikey,
        trackers: null,
        enabled: input.enabled,
        status: null,
        retry_after: null,
        search_cap: null,
        tv_search_cap: null,
        movie_search_cap: null,
        music_search_cap: null,
        audio_search_cap: null,
        book_search_cap: null,
        tv_id_caps: null,
        movie_id_caps: null,
        cat_caps: null,
        limits_caps: null,
    })
        .returning("id");
    // Query the created record
    const rawRow = await db("indexer").where({ id: result.id }).first();
    const indexer = deserializeRawRow(rawRow);
    await updateCapsForIndexer(indexer);
    const refreshedRow = await db("indexer").where({ id: indexer.id }).first();
    const refreshedIndexer = deserializeRawRow(refreshedRow);
    logger.verbose({
        label: Label.TORZNAB,
        message: `Created indexer: ${input.name || input.url}`,
    });
    return refreshedIndexer;
}
export async function updateIndexer(input) {
    const { id, ...updates } = input;
    // Prepare update object
    const updateData = {
        ...(updates.name !== undefined && { name: updates.name }),
        ...(updates.url !== undefined && { url: updates.url }),
        ...(updates.apikey !== undefined && { apikey: updates.apikey }),
        ...(updates.enabled !== undefined && { enabled: updates.enabled }),
    };
    // Atomic update with existence check
    const updateCount = await db("indexer").where({ id }).update(updateData);
    if (updateCount === 0) {
        return resultOfErr({
            code: "INDEXER_NOT_FOUND",
            message: `Indexer with ID ${id} not found`,
        });
    }
    // Query the updated record
    const updatedRawRow = await db("indexer").where({ id }).first();
    const updatedIndexer = deserializeRawRow(updatedRawRow);
    const shouldUpdateCaps = updates.url !== undefined ||
        updates.apikey !== undefined ||
        !updatedIndexer.categories;
    if (shouldUpdateCaps) {
        await updateCapsForIndexer(updatedIndexer);
    }
    const refreshedRow = await db("indexer").where({ id }).first();
    const refreshedIndexer = deserializeRawRow(refreshedRow);
    logger.verbose({
        label: Label.TORZNAB,
        message: `Updated indexer: ${refreshedIndexer.name || refreshedIndexer.url}`,
    });
    return resultOf(refreshedIndexer);
}
export async function deleteIndexer(id) {
    return db.transaction(async (trx) => {
        const rawRow = await trx("indexer").where({ id }).first();
        if (!rawRow) {
            return resultOfErr({
                code: "INDEXER_NOT_FOUND",
                message: `Indexer with ID ${id} not found`,
            });
        }
        const indexer = deserializeRawRow(rawRow);
        await trx("timestamp").where({ indexer_id: id }).del();
        await trx("rss").where({ indexer_id: id }).del();
        await trx("indexer").where({ id }).del();
        logger.verbose({
            label: Label.TORZNAB,
            message: `Deleted indexer: ${indexer.name || indexer.url}`,
        });
        return resultOf({ success: true, indexer });
    });
}
export async function getIndexerById(id) {
    const rawRow = await db("indexer").where({ id }).first();
    if (!rawRow) {
        return resultOfErr({
            code: "INDEXER_NOT_FOUND",
            message: `Indexer with ID ${id} not found`,
        });
    }
    return resultOf(deserializeRawRow(rawRow));
}
// TODO: This is a thin wrapper around getAllIndexers from ../indexers.js
// Consider consolidating duplicate functionality between indexerService and indexers modules
export async function listAllIndexers() {
    return getAllIndexers();
}
export async function mergeDisabledIndexer(sourceId, targetId) {
    if (sourceId === targetId) {
        return resultOfErr({
            code: "SAME_INDEXER",
            message: "Source and target indexers must be different",
        });
    }
    return db.transaction(async (trx) => {
        const sourceIndexer = await trx("indexer")
            .select("id", "enabled")
            .where({ id: sourceId })
            .first();
        if (!sourceIndexer) {
            return resultOfErr({
                code: "SOURCE_NOT_FOUND",
                message: `Disabled indexer with ID ${sourceId} not found`,
            });
        }
        if (sourceIndexer.enabled) {
            return resultOfErr({
                code: "INVALID_SOURCE_STATE",
                message: "Source indexer must be disabled before merging",
            });
        }
        const targetIndexer = await trx("indexer")
            .select("id", "enabled")
            .where({ id: targetId })
            .first();
        if (!targetIndexer) {
            return resultOfErr({
                code: "TARGET_NOT_FOUND",
                message: `Target indexer with ID ${targetId} not found`,
            });
        }
        if (!targetIndexer.enabled) {
            return resultOfErr({
                code: "INVALID_TARGET_STATE",
                message: "Target indexer must be enabled",
            });
        }
        const sourceTimestamps = await trx("timestamp")
            .where({ indexer_id: sourceId })
            .select("searchee_id as searcheeId", "first_searched as firstSearched", "last_searched as lastSearched");
        if (sourceTimestamps.length > 0) {
            const rows = sourceTimestamps.map((row) => ({
                indexer_id: targetId,
                searchee_id: row.searcheeId,
                first_searched: row.firstSearched,
                last_searched: row.lastSearched,
            }));
            await trx("timestamp")
                .insert(rows)
                .onConflict(["searchee_id", "indexer_id"])
                .merge({
                first_searched: trx.raw("MIN(timestamp.first_searched, excluded.first_searched)"),
                last_searched: trx.raw("MAX(timestamp.last_searched, excluded.last_searched)"),
            });
            await trx("timestamp").where({ indexer_id: sourceId }).del();
            logger.verbose({
                label: Label.TORZNAB,
                message: `Merged ${sourceTimestamps.length} timestamp rows from indexer ${sourceId} into ${targetId}`,
            });
        }
        await trx("rss").where({ indexer_id: sourceId }).del();
        const deleted = await trx("indexer").where({ id: sourceId }).del();
        if (deleted) {
            logger.verbose({
                label: Label.TORZNAB,
                message: `Deleted disabled indexer ${sourceId} after merge into ${targetId}`,
            });
        }
        return resultOf({
            mergedCount: sourceTimestamps.length,
            deleted: Boolean(deleted),
        });
    });
}
export async function testExistingIndexer(id) {
    const indexerResult = await getIndexerById(id);
    if (indexerResult.isErr()) {
        return indexerResult;
    }
    const indexer = indexerResult.unwrap();
    return testIndexerConnection(indexer.url, indexer.apikey, indexer.name || indexer.url);
}
export async function testNewIndexer(input) {
    return testIndexerConnection(input.url, input.apikey, input.url);
}
//# sourceMappingURL=indexerService.js.map