"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
    return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (g && (g = 0, op[0] && (_ = 0)), _) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateVisitorNicknameSchema = exports.visitorHistoryQuerySchema = exports.listVisitorClientsQuerySchema = exports.trackVisitSchema = exports.VISITOR_HISTORY_TABLE = exports.VISITOR_CLIENT_TABLE = void 0;
exports.ensureVisitorHistoryTables = ensureVisitorHistoryTables;
exports.getVisitorClientByClientId = getVisitorClientByClientId;
exports.listVisitorClients = listVisitorClients;
exports.listVisitorHistories = listVisitorHistories;
exports.updateVisitorNickname = updateVisitorNickname;
exports.trackVisit = trackVisit;
var zod_1 = require("zod");
var client_js_1 = require("../db/client.js");
exports.VISITOR_CLIENT_TABLE = 'visitor_clients';
exports.VISITOR_HISTORY_TABLE = 'visitor_visit_histories';
exports.trackVisitSchema = zod_1.z.object({
    clientId: zod_1.z.string().trim().min(1).max(120),
    url: zod_1.z.string().trim().min(1).max(2000),
    eventType: zod_1.z.string().trim().min(1).max(80).default('page_view'),
    userAgent: zod_1.z.string().trim().max(1000).optional().nullable(),
});
exports.listVisitorClientsQuerySchema = zod_1.z.object({
    limit: zod_1.z.coerce.number().int().min(1).max(500).optional(),
    search: zod_1.z.string().trim().max(120).optional(),
    clientId: zod_1.z.string().trim().max(120).optional(),
    nickname: zod_1.z.string().trim().max(80).optional(),
    path: zod_1.z.string().trim().max(500).optional(),
    visitedFrom: zod_1.z.string().trim().max(40).optional(),
    visitedTo: zod_1.z.string().trim().max(40).optional(),
});
exports.visitorHistoryQuerySchema = zod_1.z.object({
    limit: zod_1.z.coerce.number().int().min(1).max(500).optional(),
});
exports.updateVisitorNicknameSchema = zod_1.z.object({
    nickname: zod_1.z.string().trim().min(1).max(80),
});
function mapVisitorClientRow(row) {
    var _a, _b, _c, _d, _e, _f, _g;
    return {
        clientId: String((_a = row.client_id) !== null && _a !== void 0 ? _a : ''),
        nickname: String((_b = row.nickname) !== null && _b !== void 0 ? _b : ''),
        firstVisitedAt: String((_c = row.first_visited_at) !== null && _c !== void 0 ? _c : ''),
        lastVisitedAt: String((_d = row.last_visited_at) !== null && _d !== void 0 ? _d : ''),
        visitCount: Number((_e = row.visit_count) !== null && _e !== void 0 ? _e : 0),
        lastVisitedUrl: row.last_visited_url ? String(row.last_visited_url) : null,
        lastUserAgent: row.last_user_agent ? String(row.last_user_agent) : null,
        lastIp: row.last_ip ? String(row.last_ip) : null,
        createdAt: String((_f = row.created_at) !== null && _f !== void 0 ? _f : ''),
        updatedAt: String((_g = row.updated_at) !== null && _g !== void 0 ? _g : ''),
    };
}
function mapVisitorHistoryRow(row) {
    var _a, _b, _c, _d, _e;
    return {
        id: Number((_a = row.id) !== null && _a !== void 0 ? _a : 0),
        clientId: String((_b = row.client_id) !== null && _b !== void 0 ? _b : ''),
        visitedAt: String((_c = row.visited_at) !== null && _c !== void 0 ? _c : ''),
        url: String((_d = row.url) !== null && _d !== void 0 ? _d : ''),
        eventType: String((_e = row.event_type) !== null && _e !== void 0 ? _e : 'page_view'),
        userAgent: row.user_agent ? String(row.user_agent) : null,
        ip: row.ip ? String(row.ip) : null,
    };
}
function normalizeDateBoundary(value, boundary) {
    if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
        return value;
    }
    return boundary === 'start' ? "".concat(value, " 00:00:00") : "".concat(value, " 23:59:59.999");
}
function ensureVisitorClientTable() {
    return __awaiter(this, void 0, void 0, function () {
        var hasTable, requiredColumns, _loop_1, _i, requiredColumns_1, _a, columnName, createColumn;
        return __generator(this, function (_b) {
            switch (_b.label) {
                case 0: return [4 /*yield*/, client_js_1.db.schema.hasTable(exports.VISITOR_CLIENT_TABLE)];
                case 1:
                    hasTable = _b.sent();
                    if (!!hasTable) return [3 /*break*/, 3];
                    return [4 /*yield*/, client_js_1.db.schema.createTable(exports.VISITOR_CLIENT_TABLE, function (table) {
                            table.increments('id').primary();
                            table.string('client_id', 120).notNullable().unique();
                            table.string('nickname', 80).notNullable();
                            table.timestamp('first_visited_at', { useTz: true }).notNullable().defaultTo(client_js_1.db.fn.now());
                            table.timestamp('last_visited_at', { useTz: true }).notNullable().defaultTo(client_js_1.db.fn.now());
                            table.integer('visit_count').notNullable().defaultTo(1);
                            table.string('last_visited_url', 2000).nullable();
                            table.string('last_user_agent', 1000).nullable();
                            table.string('last_ip', 120).nullable();
                            table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(client_js_1.db.fn.now());
                            table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(client_js_1.db.fn.now());
                        })];
                case 2:
                    _b.sent();
                    return [2 /*return*/];
                case 3:
                    requiredColumns = [
                        ['client_id', function (table) { return table.string('client_id', 120).notNullable().unique(); }],
                        ['nickname', function (table) { return table.string('nickname', 80).notNullable().defaultTo('방문자_0001'); }],
                        ['first_visited_at', function (table) { return table.timestamp('first_visited_at', { useTz: true }).notNullable().defaultTo(client_js_1.db.fn.now()); }],
                        ['last_visited_at', function (table) { return table.timestamp('last_visited_at', { useTz: true }).notNullable().defaultTo(client_js_1.db.fn.now()); }],
                        ['visit_count', function (table) { return table.integer('visit_count').notNullable().defaultTo(1); }],
                        ['last_visited_url', function (table) { return table.string('last_visited_url', 2000).nullable(); }],
                        ['last_user_agent', function (table) { return table.string('last_user_agent', 1000).nullable(); }],
                        ['last_ip', function (table) { return table.string('last_ip', 120).nullable(); }],
                        ['created_at', function (table) { return table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(client_js_1.db.fn.now()); }],
                        ['updated_at', function (table) { return table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(client_js_1.db.fn.now()); }],
                    ];
                    _loop_1 = function (columnName, createColumn) {
                        var hasColumn;
                        return __generator(this, function (_c) {
                            switch (_c.label) {
                                case 0: return [4 /*yield*/, client_js_1.db.schema.hasColumn(exports.VISITOR_CLIENT_TABLE, columnName)];
                                case 1:
                                    hasColumn = _c.sent();
                                    if (!!hasColumn) return [3 /*break*/, 3];
                                    return [4 /*yield*/, client_js_1.db.schema.alterTable(exports.VISITOR_CLIENT_TABLE, function (table) {
                                            createColumn(table);
                                        })];
                                case 2:
                                    _c.sent();
                                    _c.label = 3;
                                case 3: return [2 /*return*/];
                            }
                        });
                    };
                    _i = 0, requiredColumns_1 = requiredColumns;
                    _b.label = 4;
                case 4:
                    if (!(_i < requiredColumns_1.length)) return [3 /*break*/, 7];
                    _a = requiredColumns_1[_i], columnName = _a[0], createColumn = _a[1];
                    return [5 /*yield**/, _loop_1(columnName, createColumn)];
                case 5:
                    _b.sent();
                    _b.label = 6;
                case 6:
                    _i++;
                    return [3 /*break*/, 4];
                case 7: return [2 /*return*/];
            }
        });
    });
}
function ensureVisitorHistoryTable() {
    return __awaiter(this, void 0, void 0, function () {
        var hasTable, requiredColumns, _loop_2, _i, requiredColumns_2, _a, columnName, createColumn;
        return __generator(this, function (_b) {
            switch (_b.label) {
                case 0: return [4 /*yield*/, client_js_1.db.schema.hasTable(exports.VISITOR_HISTORY_TABLE)];
                case 1:
                    hasTable = _b.sent();
                    if (!!hasTable) return [3 /*break*/, 3];
                    return [4 /*yield*/, client_js_1.db.schema.createTable(exports.VISITOR_HISTORY_TABLE, function (table) {
                            table.increments('id').primary();
                            table.string('client_id', 120).notNullable().index();
                            table.timestamp('visited_at', { useTz: true }).notNullable().defaultTo(client_js_1.db.fn.now());
                            table.string('url', 2000).notNullable();
                            table.string('event_type', 80).notNullable().defaultTo('page_view');
                            table.string('user_agent', 1000).nullable();
                            table.string('ip', 120).nullable();
                        })];
                case 2:
                    _b.sent();
                    return [2 /*return*/];
                case 3:
                    requiredColumns = [
                        ['client_id', function (table) { return table.string('client_id', 120).notNullable().index(); }],
                        ['visited_at', function (table) { return table.timestamp('visited_at', { useTz: true }).notNullable().defaultTo(client_js_1.db.fn.now()); }],
                        ['url', function (table) { return table.string('url', 2000).notNullable().defaultTo(''); }],
                        ['event_type', function (table) { return table.string('event_type', 80).notNullable().defaultTo('page_view'); }],
                        ['user_agent', function (table) { return table.string('user_agent', 1000).nullable(); }],
                        ['ip', function (table) { return table.string('ip', 120).nullable(); }],
                    ];
                    _loop_2 = function (columnName, createColumn) {
                        var hasColumn;
                        return __generator(this, function (_c) {
                            switch (_c.label) {
                                case 0: return [4 /*yield*/, client_js_1.db.schema.hasColumn(exports.VISITOR_HISTORY_TABLE, columnName)];
                                case 1:
                                    hasColumn = _c.sent();
                                    if (!!hasColumn) return [3 /*break*/, 3];
                                    return [4 /*yield*/, client_js_1.db.schema.alterTable(exports.VISITOR_HISTORY_TABLE, function (table) {
                                            createColumn(table);
                                        })];
                                case 2:
                                    _c.sent();
                                    _c.label = 3;
                                case 3: return [2 /*return*/];
                            }
                        });
                    };
                    _i = 0, requiredColumns_2 = requiredColumns;
                    _b.label = 4;
                case 4:
                    if (!(_i < requiredColumns_2.length)) return [3 /*break*/, 7];
                    _a = requiredColumns_2[_i], columnName = _a[0], createColumn = _a[1];
                    return [5 /*yield**/, _loop_2(columnName, createColumn)];
                case 5:
                    _b.sent();
                    _b.label = 6;
                case 6:
                    _i++;
                    return [3 /*break*/, 4];
                case 7: return [2 /*return*/];
            }
        });
    });
}
function ensureVisitorHistoryTables() {
    return __awaiter(this, void 0, void 0, function () {
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, ensureVisitorClientTable()];
                case 1:
                    _a.sent();
                    return [4 /*yield*/, ensureVisitorHistoryTable()];
                case 2:
                    _a.sent();
                    return [2 /*return*/];
            }
        });
    });
}
function generateAutoNickname() {
    return __awaiter(this, void 0, void 0, function () {
        var result, nextNumber;
        var _a, _b, _c;
        return __generator(this, function (_d) {
            switch (_d.label) {
                case 0: return [4 /*yield*/, client_js_1.db.raw("\n      select coalesce(max(cast(substring(nickname from '[0-9]+$') as integer)), 0) + 1 as next_nickname_number\n      from ".concat(exports.VISITOR_CLIENT_TABLE, "\n      where nickname like '\uBC29\uBB38\uC790\\_%'\n    "))];
                case 1:
                    result = _d.sent();
                    nextNumber = Number((_c = (_b = (_a = result.rows) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.next_nickname_number) !== null && _c !== void 0 ? _c : 1);
                    return [2 /*return*/, "\uBC29\uBB38\uC790_".concat(String(nextNumber).padStart(4, '0'))];
            }
        });
    });
}
function getVisitorClientByClientId(clientId) {
    return __awaiter(this, void 0, void 0, function () {
        var row;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, ensureVisitorHistoryTables()];
                case 1:
                    _a.sent();
                    return [4 /*yield*/, (0, client_js_1.db)(exports.VISITOR_CLIENT_TABLE).where({ client_id: clientId }).first()];
                case 2:
                    row = _a.sent();
                    return [2 /*return*/, row ? mapVisitorClientRow(row) : null];
            }
        });
    });
}
function listVisitorClients() {
    return __awaiter(this, arguments, void 0, function (limit, filters) {
        var query, normalizedSearch, normalizedClientId, normalizedNickname, normalizedPath, normalizedVisitedFrom, normalizedVisitedTo, rows;
        var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
        if (limit === void 0) { limit = 100; }
        if (filters === void 0) { filters = {}; }
        return __generator(this, function (_o) {
            switch (_o.label) {
                case 0: return [4 /*yield*/, ensureVisitorHistoryTables()];
                case 1:
                    _o.sent();
                    query = (0, client_js_1.db)(exports.VISITOR_CLIENT_TABLE)
                        .select('*')
                        .orderBy('last_visited_at', 'desc')
                        .limit(Math.min(Math.max(Math.trunc(limit), 1), 500));
                    normalizedSearch = (_b = (_a = filters.search) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : '';
                    if (normalizedSearch) {
                        query.where(function (builder) {
                            builder.whereILike('client_id', "%".concat(normalizedSearch, "%")).orWhereILike('nickname', "%".concat(normalizedSearch, "%"));
                        });
                    }
                    normalizedClientId = (_d = (_c = filters.clientId) === null || _c === void 0 ? void 0 : _c.trim()) !== null && _d !== void 0 ? _d : '';
                    if (normalizedClientId) {
                        query.whereILike('client_id', "%".concat(normalizedClientId, "%"));
                    }
                    normalizedNickname = (_f = (_e = filters.nickname) === null || _e === void 0 ? void 0 : _e.trim()) !== null && _f !== void 0 ? _f : '';
                    if (normalizedNickname) {
                        query.whereILike('nickname', "%".concat(normalizedNickname, "%"));
                    }
                    normalizedPath = (_h = (_g = filters.path) === null || _g === void 0 ? void 0 : _g.trim()) !== null && _h !== void 0 ? _h : '';
                    normalizedVisitedFrom = (_k = (_j = filters.visitedFrom) === null || _j === void 0 ? void 0 : _j.trim()) !== null && _k !== void 0 ? _k : '';
                    normalizedVisitedTo = (_m = (_l = filters.visitedTo) === null || _l === void 0 ? void 0 : _l.trim()) !== null && _m !== void 0 ? _m : '';
                    if (normalizedPath || normalizedVisitedFrom || normalizedVisitedTo) {
                        query.whereIn('client_id', (0, client_js_1.db)(exports.VISITOR_HISTORY_TABLE)
                            .select('client_id')
                            .modify(function (historyQuery) {
                            if (normalizedPath) {
                                historyQuery.whereILike('url', "%".concat(normalizedPath, "%"));
                            }
                            if (normalizedVisitedFrom) {
                                historyQuery.where('visited_at', '>=', normalizeDateBoundary(normalizedVisitedFrom, 'start'));
                            }
                            if (normalizedVisitedTo) {
                                historyQuery.where('visited_at', '<=', normalizeDateBoundary(normalizedVisitedTo, 'end'));
                            }
                        }));
                    }
                    return [4 /*yield*/, query];
                case 2:
                    rows = _o.sent();
                    return [2 /*return*/, rows.map(function (row) { return mapVisitorClientRow(row); })];
            }
        });
    });
}
function listVisitorHistories(clientId_1) {
    return __awaiter(this, arguments, void 0, function (clientId, limit) {
        var rows;
        if (limit === void 0) { limit = 100; }
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, ensureVisitorHistoryTables()];
                case 1:
                    _a.sent();
                    return [4 /*yield*/, (0, client_js_1.db)(exports.VISITOR_HISTORY_TABLE)
                            .select('*')
                            .where({ client_id: clientId })
                            .orderBy('visited_at', 'desc')
                            .limit(Math.min(Math.max(Math.trunc(limit), 1), 500))];
                case 2:
                    rows = _a.sent();
                    return [2 /*return*/, rows.map(function (row) { return mapVisitorHistoryRow(row); })];
            }
        });
    });
}
function updateVisitorNickname(clientId, nickname) {
    return __awaiter(this, void 0, void 0, function () {
        var rows, row;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, ensureVisitorHistoryTables()];
                case 1:
                    _a.sent();
                    return [4 /*yield*/, (0, client_js_1.db)(exports.VISITOR_CLIENT_TABLE)
                            .where({ client_id: clientId })
                            .update({
                            nickname: nickname,
                            updated_at: client_js_1.db.fn.now(),
                        })
                            .returning('*')];
                case 2:
                    rows = _a.sent();
                    row = rows[0];
                    return [2 /*return*/, row ? mapVisitorClientRow(row) : null];
            }
        });
    });
}
function trackVisit(payload, ip) {
    return __awaiter(this, void 0, void 0, function () {
        var parsedPayload, normalizedIp, normalizedUserAgent, existing, _a, _b, error_1, dbError;
        var _c;
        var _d;
        return __generator(this, function (_e) {
            switch (_e.label) {
                case 0: return [4 /*yield*/, ensureVisitorHistoryTables()];
                case 1:
                    _e.sent();
                    parsedPayload = exports.trackVisitSchema.parse(payload);
                    normalizedIp = String(ip !== null && ip !== void 0 ? ip : '').trim() || null;
                    normalizedUserAgent = ((_d = parsedPayload.userAgent) === null || _d === void 0 ? void 0 : _d.trim()) || null;
                    return [4 /*yield*/, (0, client_js_1.db)(exports.VISITOR_CLIENT_TABLE)
                            .select('*')
                            .where({ client_id: parsedPayload.clientId })
                            .first()];
                case 2:
                    existing = _e.sent();
                    if (!existing) return [3 /*break*/, 4];
                    return [4 /*yield*/, (0, client_js_1.db)(exports.VISITOR_CLIENT_TABLE)
                            .where({ client_id: parsedPayload.clientId })
                            .update({
                            last_visited_at: client_js_1.db.fn.now(),
                            visit_count: client_js_1.db.raw('coalesce(visit_count, 0) + 1'),
                            last_visited_url: parsedPayload.url,
                            last_user_agent: normalizedUserAgent,
                            last_ip: normalizedIp,
                            updated_at: client_js_1.db.fn.now(),
                        })];
                case 3:
                    _e.sent();
                    return [3 /*break*/, 9];
                case 4:
                    _e.trys.push([4, 7, , 9]);
                    _b = (_a = (0, client_js_1.db)(exports.VISITOR_CLIENT_TABLE)).insert;
                    _c = {
                        client_id: parsedPayload.clientId
                    };
                    return [4 /*yield*/, generateAutoNickname()];
                case 5: 
                // 최초 방문만 자동 닉네임을 발급하고 이후에는 사용자가 수정할 수 있습니다.
                return [4 /*yield*/, _b.apply(_a, [(_c.nickname = _e.sent(),
                            _c.first_visited_at = client_js_1.db.fn.now(),
                            _c.last_visited_at = client_js_1.db.fn.now(),
                            _c.visit_count = 1,
                            _c.last_visited_url = parsedPayload.url,
                            _c.last_user_agent = normalizedUserAgent,
                            _c.last_ip = normalizedIp,
                            _c.created_at = client_js_1.db.fn.now(),
                            _c.updated_at = client_js_1.db.fn.now(),
                            _c)])];
                case 6:
                    // 최초 방문만 자동 닉네임을 발급하고 이후에는 사용자가 수정할 수 있습니다.
                    _e.sent();
                    return [3 /*break*/, 9];
                case 7:
                    error_1 = _e.sent();
                    dbError = error_1;
                    if (dbError.code !== '23505') {
                        throw error_1;
                    }
                    return [4 /*yield*/, (0, client_js_1.db)(exports.VISITOR_CLIENT_TABLE)
                            .where({ client_id: parsedPayload.clientId })
                            .update({
                            last_visited_at: client_js_1.db.fn.now(),
                            visit_count: client_js_1.db.raw('coalesce(visit_count, 0) + 1'),
                            last_visited_url: parsedPayload.url,
                            last_user_agent: normalizedUserAgent,
                            last_ip: normalizedIp,
                            updated_at: client_js_1.db.fn.now(),
                        })];
                case 8:
                    _e.sent();
                    return [3 /*break*/, 9];
                case 9: return [4 /*yield*/, (0, client_js_1.db)(exports.VISITOR_HISTORY_TABLE).insert({
                        client_id: parsedPayload.clientId,
                        visited_at: client_js_1.db.fn.now(),
                        url: parsedPayload.url,
                        event_type: parsedPayload.eventType,
                        user_agent: normalizedUserAgent,
                        ip: normalizedIp,
                    })];
                case 10:
                    _e.sent();
                    return [2 /*return*/, getVisitorClientByClientId(parsedPayload.clientId)];
            }
        });
    });
}
