/**
 * API Mock Interceptor
 *
 * Serves mock data from `frontend/mocking/<endpoint>.json` when `__MOCK__` is enabled.
 * Supports tibi-server-style filtering, single-item retrieval by ID, sorting,
 * limit/offset pagination, and projection.
 *
 * Activate via build flag: `MOCK=1 yarn start`
 * For production builds, the entire module is eliminated by dead-code removal.
 */

// ---------------------------------------------------------------------------
// Mock data registry — static imports for tree-shaking compatibility.
// Add new collections here as needed.
// ---------------------------------------------------------------------------
import contentData from "../../mocking/content.json"
import medialibData from "../../mocking/medialib.json"
import navigationData from "../../mocking/navigation.json"
import commentsData from "../../mocking/comment.json"

type EJsonObjectId = {
    $oid: string
}

const mockRegistry: Record<string, Record<string, unknown>[]> = {
    content: normalizeMockCollection(contentData as Record<string, unknown>[]),
    medialib: normalizeMockCollection(medialibData as Record<string, unknown>[]),
    navigation: normalizeMockCollection(navigationData as Record<string, unknown>[]),
    comment: normalizeMockCollection(commentsData as Record<string, unknown>[]),
}

function isEJsonObjectId(value: unknown): value is EJsonObjectId {
    return !!value && typeof value === "object" && "$oid" in value && typeof (value as EJsonObjectId).$oid === "string"
}

function normalizeMockCollection(entries: Record<string, unknown>[]): Record<string, unknown>[] {
    return entries.map((entry) => normalizeMockValue(entry))
}

function normalizeMockValue<T>(value: T): T {
    if (Array.isArray(value)) {
        return value.map((item) => normalizeMockValue(item)) as T
    }

    if (!value || typeof value !== "object") {
        return value
    }

    const normalized: Record<string, unknown> = {}
    for (const [key, nestedValue] of Object.entries(value as Record<string, unknown>)) {
        normalized[key] = normalizeMockValue(nestedValue)
    }

    if (isEJsonObjectId(normalized._id)) {
        normalized._id = normalized._id.$oid
    }

    if (typeof normalized._id === "string" && normalized.id === undefined) {
        normalized.id = normalized._id
    }

    return normalized as T
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/**
 * Attempt to serve the request from mock data.
 * Returns an `ApiResult`-shaped object or `null` if no mock data is registered.
 *
 * @param endpoint  Raw endpoint string, e.g. `"content"` or `"content/home"`
 * @param options   ApiOptions (filter, sort, limit, offset, projection)
 */
export function mockApiRequest(endpoint: string, options?: ApiOptions, _body?: unknown): ApiResult<unknown> | null {
    // Strip leading `/api/` or `/` if present (callers may include it)
    const cleaned = endpoint.replace(/^\/?(api\/)?/, "")

    // Split into collection name and optional ID: "content/home" → ["content", "home"]
    const segments = cleaned.split("/").filter(Boolean)
    const collection = segments[0]
    const itemId = segments[1] // undefined for list requests

    const sourceData = mockRegistry[collection]
    if (!sourceData) return null // no mock registered → interceptor should 404

    console.log(`[mock] ${itemId ? "GET" : "LIST"} /${collection}${itemId ? "/" + itemId : ""}`, options?.filter ?? "")

    // --- Single-item retrieval ---
    if (itemId) {
        const item = sourceData.find((e) => e.id === itemId || e._id === itemId)
        let resultItem = item ? cloneEntry(item) : null
        if (resultItem) {
            resultItem = applyAggregate(resultItem, options)
            resultItem = applyLookups(resultItem, options)
        }
        return {
            data: resultItem,
            count: resultItem ? 1 : 0,
            buildTime: null,
        }
    }

    // --- List retrieval with filter / sort / pagination ---
    let results = [...sourceData]

    // Filter
    if (options?.filter) {
        results = results.filter((entry) => matchesFilter(entry, options.filter!))
    }

    const totalCount = results.length

    // Sort
    if (options?.sort) {
        results = applySort(results, options.sort)
    }

    // Pagination
    if (options?.offset) {
        results = results.slice(options.offset)
    }
    if (options?.limit) {
        results = results.slice(0, options.limit)
    }

    results = results.map((entry) => {
        let e = cloneEntry(entry)
        e = applyAggregate(e, options)
        e = applyLookups(e, options)
        return e
    })

    // Projection
    if (options?.projection) {
        results = applyProjection(results, options.projection)
    }

    return {
        data: results,
        count: totalCount,
        buildTime: null,
    }
}

// ---------------------------------------------------------------------------
// Filter engine — simple MongoDB-style matching
// ---------------------------------------------------------------------------

function matchesFilter(entry: Record<string, unknown>, filter: MongoFilter): boolean {
    for (const key of Object.keys(filter)) {
        const condition = filter[key]

        // Logical operators
        if (key === "$and") {
            if (!Array.isArray(condition)) return false
            return condition.every((sub: MongoFilter) => matchesFilter(entry, sub))
        }
        if (key === "$or") {
            if (!Array.isArray(condition)) return false
            return condition.some((sub: MongoFilter) => matchesFilter(entry, sub))
        }
        if (key === "$nor") {
            if (!Array.isArray(condition)) return false
            return !condition.some((sub: MongoFilter) => matchesFilter(entry, sub))
        }

        const value = getNestedValue(entry, key)

        // Operator object: { field: { $in: [...], $ne: "x", ... } }
        if (condition !== null && typeof condition === "object" && !Array.isArray(condition)) {
            if (!matchesOperators(value, condition as Record<string, unknown>)) return false
            continue
        }

        // Direct equality (or array-contains)
        if (Array.isArray(value)) {
            if (!value.includes(condition)) return false
        } else {
            if (value !== condition) return false
        }
    }
    return true
}

function matchesOperators(value: unknown, operators: Record<string, unknown>): boolean {
    for (const op of Object.keys(operators)) {
        const expected = operators[op]
        switch (op) {
            case "$eq":
                if (value !== expected) return false
                break
            case "$ne":
                if (value === expected) return false
                break
            case "$in":
                if (!Array.isArray(expected) || !expected.includes(value)) return false
                break
            case "$nin":
                if (Array.isArray(expected) && expected.includes(value)) return false
                break
            case "$exists":
                if (expected ? value === undefined : value !== undefined) return false
                break
            case "$gt":
                if (typeof value !== "number" || typeof expected !== "number" || value <= expected) return false
                break
            case "$gte":
                if (typeof value !== "number" || typeof expected !== "number" || value < expected) return false
                break
            case "$lt":
                if (typeof value !== "number" || typeof expected !== "number" || value >= expected) return false
                break
            case "$lte":
                if (typeof value !== "number" || typeof expected !== "number" || value > expected) return false
                break
            case "$regex": {
                const flags = (operators.$options as string) || ""
                const re = new RegExp(expected as string, flags)
                if (typeof value !== "string" || !re.test(value)) return false
                break
            }
            default:
                // Unknown operator — skip gracefully
                break
        }
    }
    return true
}

/** Resolve dot-notation paths: `"meta.title"` → `entry.meta.title` */
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
    return path.split(".").reduce<unknown>((acc, part) => {
        if (acc !== null && typeof acc === "object") return (acc as Record<string, unknown>)[part]
        return undefined
    }, obj)
}

function cloneEntry<T>(entry: T): T {
    return JSON.parse(JSON.stringify(entry)) as T
}

function applyAggregate(entry: Record<string, unknown>, options?: ApiOptions): Record<string, unknown> {
    const rawAggregate = options?.aggregate || options?.params?.aggregate
    if (!rawAggregate) return entry

    const aggregates = Array.isArray(rawAggregate)
        ? rawAggregate
        : typeof rawAggregate === "string"
          ? rawAggregate.split(",")
          : []

    if (!entry._aggregate) {
        entry._aggregate = {}
    }

    for (const spec of aggregates) {
        try {
            // Check for json object first
            if (spec.startsWith("{")) {
                // Not supported in this simple mock
                continue
            }

            // "comment:contentId:count"
            const parts = spec.split(":")
            if (parts.length < 3) continue
            const targetCollection = parts[0]
            const foreignField = parts[1]
            const operator = parts[2]

            const lookupSource = mockRegistry[targetCollection]
            if (!lookupSource) continue

            const localId = entry._id
                ? typeof (entry._id as any).$oid === "string"
                    ? (entry._id as any).$oid
                    : entry._id.toString()
                : null

            if (operator === "count") {
                const count = lookupSource.filter((item) => {
                    const itemForeignId = item[foreignField]
                        ? typeof (item[foreignField] as any).$oid === "string"
                            ? (item[foreignField] as any).$oid
                            : (item[foreignField] as any).toString()
                        : null
                    return itemForeignId === localId && item.active !== false // basic mock rule
                }).length

                ;(entry._aggregate as any)[`${targetCollection}Count`] = count
            }
        } catch (e) {
            console.warn("Mock aggregate error", e)
        }
    }
    return entry
}

function applyLookups(entry: Record<string, unknown>, options?: ApiOptions): Record<string, unknown> {
    const lookupSpecs = parseLookupSpecs(options)
    if (!lookupSpecs.length) return entry

    for (const spec of lookupSpecs) {
        const [fieldPath, collection] = spec.split(":")
        if (!fieldPath || !collection) continue

        const lookupSource = mockRegistry[collection]
        if (!lookupSource) continue

        applyLookupAtPath(entry, fieldPath.split("."), lookupSource)
    }

    return entry
}

function parseLookupSpecs(options?: ApiOptions): string[] {
    const rawLookup = [options?.lookup, options?.params?.lookup]
        .filter((value): value is string => typeof value === "string" && value.length > 0)
        .flatMap((value) => value.split(","))
        .map((value) => value.trim())
        .filter(Boolean)

    return Array.from(new Set(rawLookup))
}

function applyLookupAtPath(
    current: Record<string, unknown>,
    pathSegments: string[],
    lookupSource: Record<string, unknown>[]
): void {
    const [segment, ...rest] = pathSegments
    if (!segment) return

    const value = current[segment]

    if (rest.length === 0) {
        current._lookup = (current._lookup as Record<string, unknown> | undefined) || {}
        ;(current._lookup as Record<string, unknown>)[segment] = resolveLookupValue(value, lookupSource)
        return
    }

    if (Array.isArray(value)) {
        value.forEach((item) => {
            if (item && typeof item === "object") {
                applyLookupAtPath(item as Record<string, unknown>, rest, lookupSource)
            }
        })
        return
    }

    if (value && typeof value === "object") {
        applyLookupAtPath(value as Record<string, unknown>, rest, lookupSource)
    }
}

function resolveLookupValue(value: unknown, lookupSource: Record<string, unknown>[]): unknown {
    if (Array.isArray(value)) {
        return value.map((entryId) => resolveLookupById(entryId, lookupSource))
    }

    return resolveLookupById(value, lookupSource)
}

function resolveLookupById(value: unknown, lookupSource: Record<string, unknown>[]): Record<string, unknown> | null {
    if (typeof value !== "string") return null

    return lookupSource.find((entry) => entry.id === value || entry._id === value) || null
}

// ---------------------------------------------------------------------------
// Sort
// ---------------------------------------------------------------------------

function applySort(data: Record<string, unknown>[], sortSpec: string): Record<string, unknown>[] {
    // tibi-server sort format: "field" (asc) or "-field" (desc), comma-separated
    const fields = sortSpec
        .split(",")
        .map((s) => s.trim())
        .filter(Boolean)

    return [...data].sort((a, b) => {
        for (const field of fields) {
            const desc = field.startsWith("-")
            const key = desc ? field.slice(1) : field
            const aVal = getNestedValue(a, key)
            const bVal = getNestedValue(b, key)
            const cmp = compareValues(aVal, bVal)
            if (cmp !== 0) return desc ? -cmp : cmp
        }
        return 0
    })
}

function compareValues(a: unknown, b: unknown): number {
    if (a === b) return 0
    if (a === undefined || a === null) return -1
    if (b === undefined || b === null) return 1
    if (typeof a === "number" && typeof b === "number") return a - b
    return String(a).localeCompare(String(b))
}

// ---------------------------------------------------------------------------
// Projection
// ---------------------------------------------------------------------------

function applyProjection(data: Record<string, unknown>[], projectionStr: string): Record<string, unknown>[] {
    // projection format: "field1,field2" (include) or "-field1,-field2" (exclude)
    const fields = projectionStr
        .split(",")
        .map((s) => s.trim())
        .filter(Boolean)
    if (fields.length === 0) return data

    const isExclude = fields[0].startsWith("-")

    return data.map((entry) => {
        if (isExclude) {
            const exclude = new Set(fields.map((f) => f.replace(/^-/, "")))
            const result: Record<string, unknown> = {}
            for (const key of Object.keys(entry)) {
                if (!exclude.has(key)) result[key] = entry[key]
            }
            return result
        } else {
            const result: Record<string, unknown> = {}
            for (const field of fields) {
                if (field in entry) result[field] = entry[field]
            }
            // Always include id/_id
            if (typeof entry.id === "string") result.id = entry.id
            else if (typeof entry._id === "string") result.id = entry._id
            if (entry._id !== undefined) result._id = entry._id
            return result
        }
    })
}
