/**
 * Copyright 2025 WHATAP project authors. All rights reserved.
 * Use of this source code is governed by a license that
 * can be found in the LICENSE file.
 */

const TraceContextManager = require("../trace/trace-context-manager");
const conf = require("../conf/configure");
const HashUtil = require("../util/hashutil");
const Logger = require("../logger");
const shimmer = require("../core/shimmer");
const AsyncSender = require('../udp/async_sender');
const PacketTypeEnum = require('../udp/packet_type_enum');

var PrismaObserver = function(agent) {
    this.agent = agent;
    this.packages = ["@prisma/client"];
};

var dbc_hash = 0;
var dbc = "";

PrismaObserver.prototype.inject = function(mod, moduleName) {
    if (mod.__whatap_observe__) {
        return;
    }

    mod.__whatap_observe__ = true;
    Logger.initPrint("PrismaObserver");

    const self = this;

    if (conf.sql_enabled === false) {
        return;
    }

    // Prisma Client 초기화 메서드 후킹
    if (mod.PrismaClient) {
        shimmer.wrap(mod, 'PrismaClient', function(originalConstructor) {
            class WhatapPrismaClient extends originalConstructor {
                constructor(...args) {
                    super(...args);
                    if (this.__whatap_observe__) return;
                    this.__whatap_observe__ = true;

                    self.setupConnectionInfo(this);

                    if (typeof this.$use === 'function') {
                        // Prisma v4: $use 미들웨어 사용
                        self.hookUseMiddleware(this);
                    } else if (typeof this._request === 'function') {
                        // Prisma v5+: _request 메서드 직접 래핑
                        self.hookRequestMethod(this);
                    }
                }
            }
            Object.defineProperty(WhatapPrismaClient, 'name', {
                value: originalConstructor.name,
            });
            return WhatapPrismaClient;
        });
    }
};

PrismaObserver.prototype._extractRawSql = function(params) {
    try {
        const clientMethod = params.clientMethod || '';
        const isRawQuery = clientMethod.startsWith('$queryRaw') ||
            clientMethod.startsWith('$executeRaw');
        if (!isRawQuery) {
            return null;
        }

        var args = params.args;

        // args 구조 디버그 로깅
        if (conf.debug_enabled) {
            try {
                Logger.print("WHATAP-PRISMA-DEBUG", "clientMethod: " + clientMethod +
                    ", args type: " + typeof args +
                    ", args: " + JSON.stringify(args), false);
            } catch (logErr) {
                Logger.print("WHATAP-PRISMA-DEBUG", "clientMethod: " + clientMethod +
                    ", args: [stringify failed]", false);
            }
        }

        if (!args) {
            return null;
        }

        // -- 공통: { query, parameters } 형태 추출 (Prisma v5 내부 변환) --
        // args 자체가 { query, parameters } 객체
        if (!Array.isArray(args) && args.query && typeof args.query === 'string') {
            return args.query;
        }
        // args[0]이 { query, parameters } 객체
        if (Array.isArray(args) && args[0] && args[0].query && typeof args[0].query === 'string') {
            return args[0].query;
        }

        // $queryRawUnsafe / $executeRawUnsafe: args = ['SELECT ...', param1, param2, ...]
        if (clientMethod.endsWith('Unsafe')) {
            if (Array.isArray(args) && typeof args[0] === 'string') {
                return args[0];
            }
            return null;
        }

        // $queryRaw / $executeRaw (tagged template):
        var templateArg = Array.isArray(args) ? args[0] : args;

        // { strings, values } 객체 (Prisma.sql 또는 tagged template)
        if (templateArg && templateArg.strings && Array.isArray(templateArg.strings)) {
            var strings = templateArg.strings;
            var values = templateArg.values || [];
            var sql = '';
            for (var i = 0; i < strings.length; i++) {
                sql += strings[i];
                if (i < values.length) {
                    sql += '$' + (i + 1);
                }
            }
            return sql;
        }

        // raw tagged template array: args = [['SELECT ...', ''], param1]
        if (Array.isArray(templateArg)) {
            var values = args.slice(1);
            var sql = '';
            for (var i = 0; i < templateArg.length; i++) {
                sql += templateArg[i];
                if (i < values.length) {
                    sql += '$' + (i + 1);
                }
            }
            return sql;
        }

        if (typeof templateArg === 'string') {
            return templateArg;
        }
    } catch (e) {
        Logger.printError("WHATAP-225", "Failed to extract raw SQL", e, false);
    }
    return null;
};

PrismaObserver.prototype.hookRequestMethod = function(prismaInstance) {
    const self = this;
    const originalRequest = prismaInstance._request;

    prismaInstance._request = function(params) {
        const ctx = TraceContextManager.getCurrentContext();
        if (!ctx) {
            return originalRequest.call(this, params);
        }

        const model = params.model;
        const action = params.action || params.clientMethod || 'unknown';

        // Raw Query인 경우 실제 SQL 추출
        const rawSql = self._extractRawSql(params);

        // queryInfo: footprint/로깅용 (메서드명)
        const queryInfo = model
            ? `Prisma ${model}.${action}`
            : `Prisma ${params.clientMethod || action}`;

        // sqlText: TX_SQL 패킷에 전송할 텍스트
        // Raw Query인 경우: "Prisma $queryRawUnsafe SELECT * FROM users ..."
        // ORM인 경우: "Prisma User.findMany"
        var sqlText;
        if (rawSql) {
            sqlText = queryInfo + ' ' + rawSql;
            // } else if (model && params.args) {
            //     try {
            //         sqlText = queryInfo + ' ' + JSON.stringify(params.args);
            //     } catch (e) {
            //         sqlText = queryInfo;
            //     }
        } else {
            sqlText = queryInfo;
        }

        ctx.start_time = Date.now();
        ctx.elapsed = 0;
        AsyncSender.send_packet(PacketTypeEnum.TX_DB_CONN, ctx, [dbc]);

        var sql_start_time = Date.now();

        ctx.footprint(`${queryInfo} Start`);
        ctx.sql_count = (ctx.sql_count || 0) + 1;
        ctx.active_sqlhash = HashUtil.hashFromString(sqlText);
        ctx.active_dbc = dbc_hash;

        try {
            const result = originalRequest.call(this, params);

            // _request는 Promise/thenable을 반환하므로 .then()으로 후처리
            if (result && typeof result.then === 'function') {
                return result.then(
                    function(res) {
                        self._finishQuery(ctx, sql_start_time, res, sqlText);
                        ctx.footprint(`${queryInfo} Done`);
                        return res;
                    },
                    function(err) {
                        self._handleError(ctx, sql_start_time, err, sqlText);
                        ctx.footprint(`${queryInfo} Error`);
                        throw err;
                    }
                );
            }

            return result;
        } catch (err) {
            self._handleError(ctx, sql_start_time, err, sqlText);
            ctx.footprint(`${queryInfo} Error`);
            throw err;
        }
    };
};

PrismaObserver.prototype.patchPrismaInstance = function(prismaInstance) {
    if (prismaInstance.__whatap_observe__) {
        return;
    }
    prismaInstance.__whatap_observe__ = true;

    this.setupConnectionInfo(prismaInstance);
    this.hookUseMiddleware(prismaInstance);
};

PrismaObserver.prototype.setupConnectionInfo = function(prismaInstance) {
    try {
        // 연결 정보 가져오기 시도
        const url = prismaInstance._engineConfig?.overrideDatasources?.db?.url ||
            prismaInstance._engineConfig?.datasources?.db?.url ||
            prismaInstance._baseDmmf?.datamodel?.datasources?.[0]?.url?.value ||
            process.env.DATABASE_URL ||
            'prisma:unknown';

        if (url && url !== "prisma:unknown" && !dbc_hash) {
            const dbUrl = new URL(url);
            const protocol = dbUrl.protocol.replace(':', '');

            // MySQL 관찰자와 동일한 형식으로 구성
            dbc = `${protocol}://`;
            dbc += dbUrl.username || '';
            dbc += "@";
            dbc += dbUrl.hostname || '';
            dbc += '/';
            dbc += dbUrl.pathname.replace('/', '') || '';
            dbc_hash = HashUtil.hashFromString(dbc);
        }

    } catch (e) {
        Logger.printError("WHATAP-223", "Failed to extract connection info", e, false);
        dbc = "prisma:unknown";
        dbc_hash = HashUtil.hashFromString(dbc);
    }
};

PrismaObserver.prototype.hookUseMiddleware = function(prismaInstance) {
    const self = this;

    if (typeof prismaInstance.$use === 'function') {
        prismaInstance.$use(async (params, next) => {
            const ctx = TraceContextManager.getCurrentContext();
            if (!ctx) {
                return next(params);
            }

            // DB 연결 패킷 전송 (mysql2-observer와 동일한 패턴)
            ctx.start_time = Date.now();
            ctx.elapsed = 0;
            AsyncSender.send_packet(PacketTypeEnum.TX_DB_CONN, ctx, [dbc]);

            // SQL 시작 시간 기록
            var sql_start_time = Date.now();

            const modelName = params.model || 'unknown';
            const action = params.action || 'unknown';

            ctx.footprint(`Prisma ${modelName}.${action} Start`);
            ctx.sql_count = (ctx.sql_count || 0) + 1;

            // 쿼리 정보 생성
            const queryInfo = `Prisma ${modelName}.${action}`;

            ctx.active_sqlhash = HashUtil.hashFromString(queryInfo);
            ctx.active_dbc = dbc_hash;

            try {
                const result = await next(params);

                self._finishQuery(ctx, sql_start_time, result, queryInfo);
                ctx.footprint(`Prisma ${modelName}.${action} Done`);

                return result;
            } catch (err) {
                self._handleError(ctx, sql_start_time, err, queryInfo);
                ctx.footprint(`Prisma ${modelName}.${action} Error`);
                throw err;
            }
        });
    }
};

PrismaObserver.prototype._finishQuery = function(ctx, sql_start_time, result, queryInfo) {
    var sql_elapsed = Date.now() - sql_start_time;
    ctx.sql_time = (ctx.sql_time || 0) + sql_elapsed;

    // 결과 개수 계산
    let resultCount = 0;
    if (result) {
        if (Array.isArray(result)) {
            resultCount = result.length;
        } else if (result && typeof result === "object" && result.count !== undefined) {
            resultCount = result.count;
        } else if (result && typeof result === "object") {
            resultCount = 1;
        }
    }

    // SQL 패킷 전송 (mysql2-observer와 동일한 패턴)
    ctx.elapsed = sql_elapsed;
    ctx.active_sqlhash = false;
    AsyncSender.send_packet(PacketTypeEnum.TX_SQL, ctx, [dbc, queryInfo, String(resultCount)]);
};

PrismaObserver.prototype._handleError = function(ctx, sql_start_time, err, queryInfo) {
    var sql_elapsed = Date.now() - sql_start_time;
    ctx.sql_time = (ctx.sql_time || 0) + sql_elapsed;

    try {
        const errorClassName = err.name || err.constructor?.name || "UnknownError";
        const errorMessage = err.message || 'Prisma error';
        let errorStack = '';

        ctx.error_class = errorClassName;
        ctx.error_message = errorMessage;

        // 스택 트레이스 처리
        if (conf.trace_sql_error_stack && conf.trace_sql_error_depth && err.stack) {
            var traceDepth = conf.trace_sql_error_depth;
            var stackLines = err.stack.split("\n");

            if (stackLines.length > traceDepth) {
                stackLines = stackLines.slice(0, traceDepth + 1);
            }
            errorStack = stackLines.join("\n");
            ctx.error_message = errorStack;
        }

        // 에러 무시 조건 확인 (mysql2-observer와 동일한 로직)
        var shouldAddError = false;
        if (conf._is_trace_ignore_err_cls_contains === true && errorClassName &&
            errorClassName.indexOf(conf.trace_ignore_err_cls_contains) < 0) {
            shouldAddError = true;
        } else if (conf._is_trace_ignore_err_msg_contains === true && errorMessage &&
            errorMessage.indexOf(conf.trace_ignore_err_msg_contains) < 0) {
            shouldAddError = true;
        } else if (conf._is_trace_ignore_err_cls_contains === false &&
            conf._is_trace_ignore_err_msg_contains === false) {
            shouldAddError = true;
        }

        if (shouldAddError) {
            if(!ctx.error) ctx.error = 1;
            ctx.status = 500;
            ctx.errClass = errorClassName;
            ctx.errMessage = errorMessage;

            var errors = [errorClassName];
            if (errorMessage) {
                errors.push(errorMessage);
            }
            if (errorStack || err.stack) {
                errors.push(errorStack || err.stack);
            }
            AsyncSender.send_packet(PacketTypeEnum.TX_ERROR, ctx, errors);
        }

        // SQL 패킷 전송 (에러 발생시에도)
        ctx.elapsed = sql_elapsed;
        ctx.active_sqlhash = false;
        AsyncSender.send_packet(PacketTypeEnum.TX_SQL, ctx, [dbc, queryInfo, "0"]);

    } catch (e) {
        Logger.printError("WHATAP-224", "Error handling failed", e, false);
    }
};

exports.PrismaObserver = PrismaObserver;