import {
    Check,
    Test,
    Suite,
    Baseline,
    CheckDocument,
    Snapshot,
} from '@models';

import { Types, Schema } from 'mongoose';
import { calculateAcceptedStatus, buildIdentObject } from '@utils';
import * as snapshotService from './snapshot.service';
import { domSnapshotService } from './dom-snapshot.service';
import * as orm from '@lib/dbItems';
import log from '@lib/logger';
import { BaselineDocument } from '@models/Baseline.model';
import { LogOpts, RequestUser } from '@root/src/types';
import { webhookService } from './webhook.service';
import { compareCheck } from './comparison.service';
import { CreateCheckParamsExtended } from '../../types/Check';

async function calculateTestStatus(testId: string): Promise<string> {
    const checksInTest = await Check.find({ test: testId });
    const statuses = checksInTest.map((x: CheckDocument) => x.status[0]);
    let testCalculatedStatus = 'Failed';
    if (statuses.every((x: string) => (x === 'new') || (x === 'passed'))) {
        testCalculatedStatus = 'Passed';
    }
    if (statuses.every((x: string) => (x === 'new'))) {
        testCalculatedStatus = 'New';
    }
    return testCalculatedStatus;
}


export interface baselineParamsType extends Document {
    snapshootId?: string;
    name: string;
    app: string;
    branch: string;
    browserName: string;
    browserVersion?: string;
    browserFullVersion?: string;
    viewport: string;
    os: string;
    markedAs?: 'bug' | 'accepted';
    lastMarkedDate?: Date;
    createdDate?: Date;
    updatedDate?: Date;
    markedById?: Schema.Types.ObjectId;
    markedByUsername?: string;
    ignoreRegions?: string;
    boundRegions?: string;
    matchType?: 'antialiasing' | 'nothing' | 'colors';
    toleranceThreshold?: number;
    meta?: object;
    actualSnapshotId: Schema.Types.ObjectId;
    markedDate: Date;
}


const validateBaselineParam = (params: baselineParamsType): void => {
    const mandatoryParams = ['markedAs', 'markedById', 'markedByUsername', 'markedDate'];
    for (const param of mandatoryParams) {
        if (!params[param as keyof baselineParamsType]) {
            const errMsg = `invalid baseline parameters, '${param}' is empty, params: ${JSON.stringify(params)}`;
            log.error(errMsg);
            throw new Error(errMsg);
        }
    }
};

async function createNewBaseline(params: baselineParamsType): Promise<BaselineDocument> {
    const logOpts = {
        scope: 'createNewBaseline',
        msgType: 'CREATE',
    };


    validateBaselineParam(params);

    const identFields = buildIdentObject(params);

    const lastBaseline = await Baseline.findOne(identFields).sort({ createdDate: -1 }).exec();
    const filter = { ...identFields, snapshootId: params.actualSnapshotId };

    const baselineParams: Record<string, unknown> = { ...identFields };
    if (lastBaseline?.ignoreRegions) {
        baselineParams.ignoreRegions = lastBaseline.ignoreRegions;
    }
    if (typeof lastBaseline?.toleranceThreshold === 'number') {
        baselineParams.toleranceThreshold = lastBaseline.toleranceThreshold;
    }

    const update = {
        $setOnInsert: {
            ...baselineParams,
            snapshootId: params.actualSnapshotId,
            createdDate: new Date(),
        },
        $set: {
            markedAs: params.markedAs,
            markedById: params.markedById,
            markedByUsername: params.markedByUsername,
            lastMarkedDate: params.markedDate,
        },
    };

    try {
        const baseline = await Baseline.findOneAndUpdate(
            filter,
            update,
            { new: true, upsert: true },
        ).exec();

        log.debug(`baseline upserted for snapshot id: ${params.actualSnapshotId}`, logOpts);
        log.silly({ baseline });
        return baseline as BaselineDocument;
    } catch (err: any) {
        if (err?.code === 11000) {
            log.warn(`baseline duplicate key detected for filter ${JSON.stringify(filter)}, retrying fetch`, logOpts);
            const existing = await Baseline.findOne(filter).exec();
            if (existing) {
                existing.markedAs = params.markedAs;
                existing.markedById = params.markedById;
                existing.markedByUsername = params.markedByUsername;
                existing.lastMarkedDate = params.markedDate;
                existing.createdDate = new Date();
                existing.snapshootId = params.actualSnapshotId;
                return existing.save();
            }
        }

        log.error(`cannot upsert baseline: ${err instanceof Error ? err.message : String(err)}`, logOpts);
        throw err;
    }
}

const extractSnapshotId = (snapshot: unknown): string | undefined => {
    if (!snapshot) return undefined;
    if (typeof snapshot === 'string') return snapshot;
    if (typeof snapshot === 'object') {
        const snapshotObj = snapshot as { _id?: unknown, id?: unknown, toString?: () => string };
        if (snapshotObj._id) return String(snapshotObj._id);
        if (snapshotObj.id) return String(snapshotObj.id);
        if (typeof snapshotObj.toString === 'function') return snapshotObj.toString();
    }
    return undefined;
};

const unwrapIdentValue = (
    value: unknown,
    visited: WeakSet<object> = new WeakSet(),
): unknown => {
    if (!value) return undefined;
    if (typeof value !== 'object') return value;
    if (value instanceof Types.ObjectId || (value as { _bsontype?: string })?._bsontype === 'ObjectID') {
        return value;
    }
    const obj = value as { _id?: unknown, id?: unknown };
    if (visited.has(obj)) return undefined;
    visited.add(obj);

    if (obj._id && obj._id !== value) {
        return unwrapIdentValue(obj._id, visited);
    }
    if (obj.id && obj.id !== value) {
        return unwrapIdentValue(obj.id, visited);
    }
    return value;
};

const extractIdentValueAsString = (value: unknown): string => {
    const unwrapped = unwrapIdentValue(value);
    if (!unwrapped) return '';
    if (typeof unwrapped === 'string') return unwrapped;
    if (unwrapped instanceof Types.ObjectId || (unwrapped as { _bsontype?: string })?._bsontype === 'ObjectID') {
        return unwrapped.toString();
    }
    return String(unwrapped);
};

const normalizeIdentValueForQuery = (field: string, value: unknown): unknown => {
    if (value === undefined || value === null) return undefined;
    const unwrapped = unwrapIdentValue(value);
    if (field === 'app') {
        if (unwrapped instanceof Types.ObjectId || (unwrapped as { _bsontype?: string })?._bsontype === 'ObjectID') {
            return unwrapped;
        }
        const strValue = extractIdentValueAsString(unwrapped);
        if (!strValue) return undefined;
        return Types.ObjectId.isValid(strValue) ? new Types.ObjectId(strValue) : strValue;
    }
    return extractIdentValueAsString(unwrapped);
};

const enrichChecksWithCurrentAcceptance = async (
    checks: Array<CheckDocument | Record<string, unknown>>,
): Promise<Record<string, unknown>[]> => {
    if (!checks || checks.length === 0) return [];

    const plainChecks = checks.map((check) => (
        (check && typeof (check as CheckDocument).toJSON === 'function')
            ? (check as CheckDocument).toJSON()
            : { ...(check as Record<string, unknown>) }
    ));

    // Get unique combinations of ident fields to query baselines
    const identFields = ['name', 'viewport', 'browserName', 'os', 'app', 'branch'];
    const baselineQueries: Record<string, unknown>[] = [];
    const checksByIdentKey = new Map<string, Record<string, unknown>[]>();

    plainChecks.forEach((check) => {
        const identKey = identFields.map((field) => extractIdentValueAsString(check?.[field])).join('|');

        if (!checksByIdentKey.has(identKey)) {
            checksByIdentKey.set(identKey, []);

            // Build query for this ident combination
            const query: Record<string, unknown> = {};
            identFields.forEach((field) => {
                const normalized = normalizeIdentValueForQuery(field, check?.[field]);
                if (normalized !== undefined) query[field] = normalized;
            });

            // Only add query if we have all required fields
            const hasAllFields = identFields.every((field) => query[field] !== undefined);
            if (hasAllFields) {
                baselineQueries.push(query);
            } else {
                log.warn(`Check ${check._id} missing required ident fields. Has: ${Object.keys(query).join(', ')}`, {
                    scope: 'enrichChecksWithCurrentAcceptance',
                });
            }
        }

        checksByIdentKey.get(identKey)?.push(check);
    });

    // Fetch the latest baseline for each unique ident combination
    const baselinesMap = new Map<string, Record<string, unknown>>();

    if (baselineQueries.length > 0) {
        try {
            const baselines = await Baseline.aggregate([
                {
                    $match: { $or: baselineQueries },
                },
                {
                    $sort: { createdDate: -1 },
                },
                {
                    $group: {
                        _id: {
                            name: '$name',
                            viewport: '$viewport',
                            browserName: '$browserName',
                            os: '$os',
                            app: '$app',
                            branch: '$branch',
                        },
                        doc: { $first: '$$ROOT' },
                    },
                },
                {
                    $replaceRoot: { newRoot: '$doc' },
                },
            ]).exec();

            baselines.forEach((baseline) => {
                const baselineObj = baseline as unknown as Record<string, unknown>;
                const identKey = identFields.map((field) => extractIdentValueAsString(baselineObj?.[field])).join('|');
                baselinesMap.set(identKey, baselineObj);
                log.debug(`[enrichChecks] Found baseline for identKey=${identKey}, snapshootId=${baselineObj.snapshootId}`, {
                    scope: 'enrichChecksWithCurrentAcceptance',
                });
            });
        } catch (err) {
            log.error(`[enrichChecks] Error fetching baselines: ${err}`, { scope: 'enrichChecksWithCurrentAcceptance' });
            throw err;
        }
    }

    // Enrich checks with acceptance flags
    return plainChecks.map((check) => {
        const identKey = identFields.map((field) => extractIdentValueAsString(check?.[field])).join('|');
        const baseline = baselinesMap.get(identKey);
        const actualSnapshotId = extractSnapshotId(check?.actualSnapshotId);
        const baselineSnapshotId = baseline ? extractSnapshotId(baseline.snapshootId) : undefined;
        const checkBaselineSnapshotId = extractSnapshotId(check?.baselineId);

        const matchesOwnBaseline = Boolean(
            actualSnapshotId
            && checkBaselineSnapshotId
            && actualSnapshotId === checkBaselineSnapshotId,
        );
        const matchesLatestBaseline = Boolean(
            actualSnapshotId
            && baselineSnapshotId
            && actualSnapshotId === baselineSnapshotId,
        );

        const isCurrentlyAccepted = Boolean(
            check?.markedAs === 'accepted'
            && (matchesOwnBaseline || matchesLatestBaseline),
        );

        const hasKnownBaseline = Boolean(checkBaselineSnapshotId || baselineSnapshotId);

        const wasAcceptedEarlier = Boolean(
            check?.markedAs === 'accepted'
            && hasKnownBaseline
            && !isCurrentlyAccepted,
        );

        // Debug logging
        if (check?.markedAs === 'accepted') {
            log.debug(`[enrichChecks] Check ${check._id}: actualSnapshot=${actualSnapshotId}, baselineSnapshot=${baselineSnapshotId}, checkBaselineSnapshot=${checkBaselineSnapshotId}, isCurrentlyAccepted=${isCurrentlyAccepted}, wasAcceptedEarlier=${wasAcceptedEarlier}, hasBaseline=${Boolean(baseline)}`, {
                scope: 'enrichChecksWithCurrentAcceptance',
            });
        }

        return {
            ...check,
            isCurrentlyAccepted,
            wasAcceptedEarlier,
        };
    });
};

const accept = async (
    id: string,
    baselineId: string,
    user: RequestUser,
): Promise<Record<string, unknown>> => {
    const logOpts = {
        msgType: 'ACCEPT',
        itemType: 'check',
        ref: id,
        user: user?.username,
        scope: 'accept',
    };
    log.debug(`accept check: ${id}`, logOpts);
    const check = await Check.findById(id).exec();
    if (!check) throw new Error(`cannot find check with id: ${id}`);
    const test = await Test.findById(check.test).exec();
    if (!test) throw new Error(`cannot find test with id: ${check.test}`);
    check.markedById = user._id;
    check.markedByUsername = user.username;
    check.markedDate = new Date();
    check.markedAs = 'accepted';
    check.status = (check.status[0] === 'new') ? ['new'] : ['passed'];
    // check.status = ['passed'];
    check.updatedDate = new Date();

    if (baselineId) {
        check.baselineId = new Types.ObjectId(baselineId);
    }

    log.debug(`update check with options: '${JSON.stringify(check.toObject())}'`, logOpts);
    const baseline = await createNewBaseline(check.toObject());

    // Link DOM snapshot to the baseline for RCA feature
    try {
        await domSnapshotService.linkDomSnapshotToBaseline(id, baseline._id.toString());
        log.debug(`DOM snapshot linked to baseline: '${baseline._id}'`, logOpts);
    } catch (domErr) {
        // DOM snapshot linking is non-critical
        log.warn(`Failed to link DOM snapshot to baseline: ${domErr}`, logOpts);
    }

    await check.save();

    const testCalculatedStatus = await calculateTestStatus(String(check.test));
    const testCalculatedAcceptedStatus = await calculateAcceptedStatus(check.test);

    test.status = testCalculatedStatus;
    test.markedAs = testCalculatedAcceptedStatus;
    test.updatedDate = new Date();

    await Suite.findByIdAndUpdate(check.suite, { updatedDate: Date.now() });
    log.debug(`update test with status: '${testCalculatedStatus}', marked: '${testCalculatedAcceptedStatus}'`, logOpts, {
        msgType: 'UPDATE',
        itemType: 'test',
        ref: test._id,
    });
    await test.save();
    await check.save();
    log.debug(`check with id: '${id}' was updated`, logOpts);
    const [enrichedCheck] = await enrichChecksWithCurrentAcceptance([check]);
    webhookService.triggerWebhooks('check.updated', enrichedCheck).catch((e) => log.error(`Webhook error: ${e}`));
    return enrichedCheck;
};

async function removeCheck(id: string, user: RequestUser): Promise<CheckDocument> {
    const logMeta = {
        scope: 'removeCheck',
        itemType: 'check',
        ref: id,
        msgType: 'REMOVE',
        user: user?.username,
    };

    try {
        const check = (await Check.findByIdAndDelete(id).exec()) as unknown as CheckDocument;
        if (!check) throw new Error(`cannot find check with id: ${id}`);

        log.debug(`check with id: '${id}' was removed, update test: ${check.test}`, logMeta);

        const test = await Test.findById(check.test).exec();
        if (!test) throw new Error(`cannot find test with id: ${check.test}`);

        const testCalculatedStatus = await calculateTestStatus(String(check.test));
        const testCalculatedAcceptedStatus = await calculateAcceptedStatus(check.test);
        test.status = testCalculatedStatus;
        test.markedAs = testCalculatedAcceptedStatus;
        test.updatedDate = new Date();
        await orm.updateItemDate('VRSSuite', check.suite);
        await test.save();

        if (check.baselineId && String(check.baselineId) !== 'undefined') {
            log.debug(`try to remove the snapshot, baseline: ${check.baselineId}`, logMeta);
            await snapshotService.remove(check.baselineId.toString());
        }

        if (check.actualSnapshotId && String(check.baselineId) !== 'undefined') {
            log.debug(`try to remove the snapshot, actual: ${check.actualSnapshotId}`, logMeta);
            await snapshotService.remove(check.actualSnapshotId.toString());
        }

        if (check.diffId && String(check.baselineId) !== 'undefined') {
            log.debug(`try to remove snapshot, diff: ${check.diffId}`, logMeta);
            await snapshotService.remove(check.diffId.toString());
        }

        // Remove DOM snapshots associated with the check
        try {
            await domSnapshotService.removeDomSnapshotsByCheckId(id);
            log.debug(`DOM snapshots removed for check: ${id}`, logMeta);
        } catch (domErr) {
            log.warn(`Failed to remove DOM snapshots for check ${id}: ${domErr}`, logMeta);
        }

        return check;
    } catch (e: unknown) {
        const errMsg = `cannot remove a check with id: '${id}', error: '${e instanceof Error ? e.stack : String(e)}'`;
        log.error(errMsg, logMeta);
        throw new Error(errMsg);
    }
}

const remove = async (id: string, user: RequestUser): Promise<CheckDocument> => {
    const logOpts = {
        scope: 'removeCheck',
        itemType: 'check',
        ref: id,
        user: user?.username,
        msgType: 'REMOVE',
    };
    log.info(`remove check with, id: '${id}', user: '${user.username}'`, logOpts);
    return removeCheck(id, user);
};

const update = async (id: string, opts: Partial<CheckDocument>, user: string): Promise<CheckDocument> => {
    const logMeta: LogOpts = {
        msgType: 'UPDATE',
        itemType: 'check',
        ref: id,
        user,
        scope: 'updateCheck',
    };
    log.debug(`update check with id '${id}' with params '${JSON.stringify(opts, null, 2)}'`, logMeta);

    const check = await Check.findOneAndUpdate({ _id: id }, opts, { new: true }).exec();
    if (!check) throw new Error(`cannot find check with id: ${id}`);

    const test = await Test.findOne({ _id: check.test }).exec();
    if (!test) throw new Error(`cannot find test with id: ${check.test}`);

    test.status = await calculateTestStatus(String(check.test));

    await orm.updateItemDate('VRSCheck', check);
    await orm.updateItemDate('VRSTest', test);
    await test.save();
    await check.save();
    webhookService.triggerWebhooks('check.updated', check).catch((e) => log.error(`Webhook error: ${e}`));
    return check;
};

const recompare = async (id: string, user: RequestUser): Promise<Record<string, unknown>> => {
    const logOpts: LogOpts = {
        scope: 'recompareCheck',
        itemType: 'check',
        ref: id,
        user: user?.username,
        msgType: 'COMPARE',
    };

    const check = await Check.findById(id).exec();
    if (!check) throw new Error(`cannot find check with id: ${id}`);

    if (!check.baselineId) {
        throw new Error(`cannot recompare check '${id}': baselineId is empty`);
    }
    if (!check.actualSnapshotId) {
        throw new Error(`cannot recompare check '${id}': actualSnapshotId is empty`);
    }

    const baselineSnapshot = await Snapshot.findById(check.baselineId).exec();
    if (!baselineSnapshot) {
        throw new Error(`cannot recompare check '${id}': baseline snapshot '${check.baselineId}' not found`);
    }

    const actualSnapshot = await Snapshot.findById(check.actualSnapshotId).exec();
    if (!actualSnapshot) {
        throw new Error(`cannot recompare check '${id}': actual snapshot '${check.actualSnapshotId}' not found`);
    }

    const oldDiffId = check.diffId ? check.diffId.toString() : null;

    const checkParamsForCompare: CreateCheckParamsExtended = {
        test: check.test.toString(),
        name: check.name,
        status: 'pending',
        viewport: check.viewport || '',
        browserName: check.browserName || '',
        browserVersion: check.browserVersion || '',
        browserFullVersion: check.browserFullVersion || '',
        os: check.os || '',
        updatedDate: Date.now(),
        suite: check.suite.toString(),
        app: check.app.toString(),
        branch: check.branch || '',
        run: check.run ? check.run.toString() : '',
        creatorId: check.creatorId ? check.creatorId.toString() : user._id.toString(),
        creatorUsername: check.creatorUsername || user.username,
        failReasons: [],
        actualSnapshotId: check.actualSnapshotId.toString(),
        hashCode: '',
        toleranceThreshold: (check as any).toleranceThreshold,
    };

    const compareResult = await compareCheck(
        baselineSnapshot,
        actualSnapshot,
        checkParamsForCompare,
        false,
        user,
    );

    check.status = [compareResult.status as ('new' | 'pending' | 'approved' | 'running' | 'passed' | 'failed' | 'aborted' | 'blinking')];
    check.result = compareResult.result;
    check.failReasons = compareResult.failReasons;
    check.updatedDate = new Date();

    if (compareResult.diffId) {
        check.diffId = new Types.ObjectId(compareResult.diffId);
    } else {
        check.diffId = undefined;
    }

    await check.save();

    const test = await Test.findById(check.test).exec();
    if (test) {
        const testCalculatedStatus = await calculateTestStatus(String(check.test));
        const testCalculatedAcceptedStatus = await calculateAcceptedStatus(check.test);
        test.status = testCalculatedStatus;
        test.markedAs = testCalculatedAcceptedStatus;
        test.updatedDate = new Date();
        await test.save();
        await Suite.findByIdAndUpdate(check.suite, { updatedDate: Date.now() });
    }

    const newDiffId = check.diffId ? check.diffId.toString() : null;
    if (oldDiffId && oldDiffId !== newDiffId) {
        snapshotService.remove(oldDiffId).catch((e) => {
            log.warn(`failed to remove old diff snapshot '${oldDiffId}': ${String(e)}`, logOpts);
        });
    }

    const [enrichedCheck] = await enrichChecksWithCurrentAcceptance([check]);
    webhookService.triggerWebhooks('check.updated', enrichedCheck).catch((e) => log.error(`Webhook error: ${e}`));
    return enrichedCheck;
};

const createCheckDocument = async (checkParams: any, session?: any): Promise<CheckDocument> => {
    const [check] = await Check.create([checkParams], { session });
    webhookService.triggerWebhooks('check.created', check).catch((e) => log.error(`Webhook error: ${e}`));
    return check;
};

export {
    accept,
    remove,
    update,
    recompare,
    enrichChecksWithCurrentAcceptance,
    createCheckDocument,
};
