#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
generate.py - acquire-core-full 레거시 C 코퍼스 결정론적 생성기
================================================================

카드 매입·정산 도메인의 TxCore(사내 TP 프레임워크) 기반 레거시 C 시스템을
아키타입별 템플릿 × 파라미터(module, program-id, difficulty tier)로 생성한다.

- 각 프로그램은 self-contained: framework/txcore(공유 라이브러리)만 의존하며
  독립적으로 .o 로 컴파일된다.
- 산출: framework/, app/{online,batch,dbio,common,include,shared}, fml/, scripts/,
  mk/, config/, db/, Makefile, inventory.json

사용법:
    python3 generate.py [--fraction F]     # F=1.0 full(~2050 files), 0.05 quick test
"""
import argparse
import json
import os
import shutil

ROOT = os.path.dirname(os.path.abspath(__file__))

# 11개 업무 모듈: code -> (한글명, 영문 transform 태그)
MODULES = [
    ("mg", "전문게이트웨이", "msg-gateway"),
    ("au", "승인한도",       "authorization"),
    ("ac", "매입",           "acquire"),
    ("rc", "대사",           "reconciliation"),
    ("vl", "정합성",         "validation"),
    ("st", "정산수수료",     "settlement"),
    ("py", "지급",           "payment"),
    ("lg", "원장",           "ledger"),
    ("mm", "마스터",         "master"),
    ("cm", "공통",           "common"),
    ("cl", "마감",           "closing"),
]
MOD_CODES = [m[0] for m in MODULES]
MOD_KNAME = {m[0]: m[1] for m in MODULES}
MOD_XFORM = {m[0]: m[2] for m in MODULES}

# 파일 타입별 목표 건수 (fraction=1.0 기준)
TARGETS = {
    "online":  350,
    "batch":   180,
    "common":  120,
    "dbio":    300,   # 이 중 11개는 모듈 표준 dbio_main
    "dbio_sql": 50,
    "headers": 600,   # core(11)+io헤더+cu헤더+standalone
    "fml":     120,
    "sh":      180,
    "mk":       90,
    "config":   60,
}

# 데모 링크 대상 (생성 산출물로 링크 검증)
ONLINE_DEMOS = ["mg", "ac", "rc", "st"]
BATCH_DEMOS  = ["rc", "cl"]


def tier_for(i):
    """~55% easy, ~30% medium, ~15% hard (결정론적)."""
    r = i % 20
    if r < 11:
        return "easy"
    if r < 17:
        return "medium"
    return "hard"


def render(tpl, **kw):
    out = tpl
    for k, v in kw.items():
        out = out.replace("@" + k + "@", str(v))
    return out


# ======================================================================
# 공유 프레임워크 소스 (검증된 슬라이스에서 그대로 이식) - raw 문자열
# ======================================================================
TXCORE_H = r'''/*
 * txcore.h - TxCore 공통 프레임워크 API
 *
 * TxCore 는 사내 표준 공통 프레임워크로, Tuxedo/ProFrame 계열 TP 모니터의
 * ATMI(tpcall/tpreturn/tpbegin) 및 FML/UBF(Fchg/Fget) 관용구를 얇게 감싼
 * 자체 구현 계층이다. 외부 TP 미들웨어 설치 없이 단독 빌드/링크된다.
 */
#ifndef TXCORE_H
#define TXCORE_H

#include <stddef.h>
#include <stdarg.h>

#ifdef __cplusplus
extern "C" {
#endif

#define TX_OK        0
#define TX_FAIL     (-1)
#define TX_ENOENT   (-2)
#define TX_EINVAL   (-3)
#define TX_EDB      (-4)
#define TX_ENOMEM   (-5)
#define TX_ENOKEY   (-6)

#define TX_LOG_DEBUG 0
#define TX_LOG_INFO  1
#define TX_LOG_WARN  2
#define TX_LOG_ERROR 3

#define TX_MAX_SLOTS 64
#define TX_KEY_LEN   32
#define TX_VAL_LEN   256

typedef struct {
    char key[TX_KEY_LEN];
    char val[TX_VAL_LEN];
    int  len;
    int  used;
} TXFIELD;

typedef struct {
    int     count;
    TXFIELD slots[TX_MAX_SLOTS];
} TXBUF;

typedef struct {
    char   name[TX_KEY_LEN];
    TXBUF *in;
    TXBUF *out;
    int    rcode;
    long   xid;
} TXSVCINFO;

typedef void (*tx_service_fn)(TXSVCINFO *ctx);

#define TX_SERVICE(name, ctx) void name(TXSVCINFO *ctx)

TXBUF *tx_buf_alloc(void);
void   tx_buf_free(TXBUF *buf);
void   tx_buf_reset(TXBUF *buf);
int    tx_buf_set(TXBUF *buf, const char *key, const char *val, int len);
int    tx_buf_sets(TXBUF *buf, const char *key, const char *val);
int    tx_buf_setlong(TXBUF *buf, const char *key, long val);
int    tx_buf_get(TXBUF *buf, const char *key, char *out, int outlen);
int    tx_buf_getlong(TXBUF *buf, const char *key, long *out);

int  tx_register(const char *name, tx_service_fn fn);
int  tx_call(const char *svc, TXBUF *in, TXBUF *out);
int  tx_return(TXSVCINFO *ctx, int rc, TXBUF *out);
int  tx_service_count(void);

int  tx_begin(void);
int  tx_commit(void);
int  tx_abort(void);
long tx_current_xid(void);

void tx_log(int level, const char *fmt, ...);
void tx_set_loglevel(int level);
const char *tx_strerror(int rc);

#ifdef __cplusplus
}
#endif

#endif /* TXCORE_H */
'''

TXCORE_DBIO_H = r'''/*
 * txcore_dbio.h - TxCore DBIO 규약 (ECPG/Pro*C 공통)
 */
#ifndef TXCORE_DBIO_H
#define TXCORE_DBIO_H

#include "txcore.h"

#define TX_SQL_OK        0
#define TX_SQL_NOTFOUND  100

#define TX_DBIO_RESULT(sc)                                   \
    ((sc) == TX_SQL_OK      ? TX_OK :                        \
     (sc) == TX_SQL_NOTFOUND ? TX_ENOENT : TX_EDB)

#define TX_DBIO_LOG_ERR(tag)                                 \
    tx_log(TX_LOG_ERROR, "DBIO 오류 [%s] sqlcode=%ld msg=%.*s", \
           (tag), (long)sqlca.sqlcode,                       \
           (int)sqlca.sqlerrm.sqlerrml, sqlca.sqlerrm.sqlerrmc)

#endif /* TXCORE_DBIO_H */
'''

TXCORE_C = r'''/*
 * txcore.c - TxCore 공통 프레임워크 구현
 */
#include "txcore.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define TX_MAX_SERVICES 512

typedef struct {
    char          name[TX_KEY_LEN];
    tx_service_fn fn;
} tx_entry_t;

static tx_entry_t g_registry[TX_MAX_SERVICES];
static int        g_registry_count = 0;

static int  g_loglevel = TX_LOG_INFO;
static long g_xid_seq  = 0;
static long g_cur_xid  = 0;

static const char *level_name(int level)
{
    switch (level) {
    case TX_LOG_DEBUG: return "DEBUG";
    case TX_LOG_INFO:  return "INFO ";
    case TX_LOG_WARN:  return "WARN ";
    case TX_LOG_ERROR: return "ERROR";
    default:           return "?????";
    }
}

void tx_set_loglevel(int level) { g_loglevel = level; }

void tx_log(int level, const char *fmt, ...)
{
    va_list ap;
    time_t     now;
    struct tm  tmv;
    char       ts[20];

    if (level < g_loglevel)
        return;

    now = time(NULL);
    localtime_r(&now, &tmv);
    strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tmv);

    fprintf(stderr, "[%s] %s ", ts, level_name(level));
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
    fputc('\n', stderr);
}

const char *tx_strerror(int rc)
{
    switch (rc) {
    case TX_OK:      return "정상";
    case TX_FAIL:    return "일반 실패";
    case TX_ENOENT:  return "서비스 미등록";
    case TX_EINVAL:  return "파라미터 오류";
    case TX_EDB:     return "DB 오류";
    case TX_ENOMEM:  return "자원 부족";
    case TX_ENOKEY:  return "버퍼 키 없음";
    default:         return "알 수 없는 오류";
    }
}

TXBUF *tx_buf_alloc(void) { return (TXBUF *)calloc(1, sizeof(TXBUF)); }
void   tx_buf_free(TXBUF *buf) { if (buf) free(buf); }
void   tx_buf_reset(TXBUF *buf) { if (buf) memset(buf, 0, sizeof(*buf)); }

static TXFIELD *find_slot(TXBUF *buf, const char *key)
{
    int i;
    for (i = 0; i < TX_MAX_SLOTS; i++) {
        if (buf->slots[i].used &&
            strncmp(buf->slots[i].key, key, TX_KEY_LEN) == 0)
            return &buf->slots[i];
    }
    return NULL;
}

static TXFIELD *alloc_slot(TXBUF *buf, const char *key)
{
    int i;
    TXFIELD *f = find_slot(buf, key);
    if (f)
        return f;
    for (i = 0; i < TX_MAX_SLOTS; i++) {
        if (!buf->slots[i].used) {
            f = &buf->slots[i];
            memset(f, 0, sizeof(*f));
            strncpy(f->key, key, TX_KEY_LEN - 1);
            f->used = 1;
            buf->count++;
            return f;
        }
    }
    return NULL;
}

int tx_buf_set(TXBUF *buf, const char *key, const char *val, int len)
{
    TXFIELD *f;
    if (!buf || !key || !val)
        return TX_EINVAL;
    if (len < 0 || len >= TX_VAL_LEN)
        return TX_ENOMEM;
    f = alloc_slot(buf, key);
    if (!f)
        return TX_ENOMEM;
    memcpy(f->val, val, len);
    f->val[len] = '\0';
    f->len = len;
    return TX_OK;
}

int tx_buf_sets(TXBUF *buf, const char *key, const char *val)
{
    if (!val)
        return TX_EINVAL;
    return tx_buf_set(buf, key, val, (int)strlen(val));
}

int tx_buf_setlong(TXBUF *buf, const char *key, long val)
{
    char tmp[32];
    snprintf(tmp, sizeof(tmp), "%ld", val);
    return tx_buf_set(buf, key, tmp, (int)strlen(tmp));
}

int tx_buf_get(TXBUF *buf, const char *key, char *out, int outlen)
{
    TXFIELD *f;
    if (!buf || !key || !out || outlen <= 0)
        return TX_EINVAL;
    f = find_slot(buf, key);
    if (!f)
        return TX_ENOKEY;
    if (f->len >= outlen)
        return TX_ENOMEM;
    memcpy(out, f->val, f->len);
    out[f->len] = '\0';
    return TX_OK;
}

int tx_buf_getlong(TXBUF *buf, const char *key, long *out)
{
    TXFIELD *f;
    char *end;
    long v;
    if (!buf || !key || !out)
        return TX_EINVAL;
    f = find_slot(buf, key);
    if (!f)
        return TX_ENOKEY;
    v = strtol(f->val, &end, 10);
    if (end == f->val)
        return TX_EINVAL;
    *out = v;
    return TX_OK;
}

int tx_register(const char *name, tx_service_fn fn)
{
    int i;
    if (!name || !fn)
        return TX_EINVAL;
    if (g_registry_count >= TX_MAX_SERVICES)
        return TX_ENOMEM;
    for (i = 0; i < g_registry_count; i++) {
        if (strncmp(g_registry[i].name, name, TX_KEY_LEN) == 0) {
            g_registry[i].fn = fn;
            return TX_OK;
        }
    }
    strncpy(g_registry[g_registry_count].name, name, TX_KEY_LEN - 1);
    g_registry[g_registry_count].fn = fn;
    g_registry_count++;
    tx_log(TX_LOG_DEBUG, "서비스 등록: %s", name);
    return TX_OK;
}

int tx_service_count(void) { return g_registry_count; }

static tx_service_fn lookup(const char *name)
{
    int i;
    for (i = 0; i < g_registry_count; i++) {
        if (strncmp(g_registry[i].name, name, TX_KEY_LEN) == 0)
            return g_registry[i].fn;
    }
    return NULL;
}

int tx_call(const char *svc, TXBUF *in, TXBUF *out)
{
    tx_service_fn fn;
    TXSVCINFO     ctx;
    if (!svc || !in || !out)
        return TX_EINVAL;
    fn = lookup(svc);
    if (!fn) {
        tx_log(TX_LOG_ERROR, "tx_call: 서비스 미등록 svc=%s", svc);
        return TX_ENOENT;
    }
    memset(&ctx, 0, sizeof(ctx));
    strncpy(ctx.name, svc, TX_KEY_LEN - 1);
    ctx.in    = in;
    ctx.out   = out;
    ctx.rcode = TX_OK;
    ctx.xid   = g_cur_xid;
    fn(&ctx);
    return ctx.rcode;
}

int tx_return(TXSVCINFO *ctx, int rc, TXBUF *out)
{
    if (!ctx)
        return TX_EINVAL;
    ctx->rcode = rc;
    if (out && ctx->out && out != ctx->out)
        memcpy(ctx->out, out, sizeof(TXBUF));
    return rc;
}

int tx_begin(void)
{
    if (g_cur_xid != 0) {
        tx_log(TX_LOG_WARN, "tx_begin: 이미 진행중 xid=%ld", g_cur_xid);
        return TX_FAIL;
    }
    g_cur_xid = ++g_xid_seq;
    tx_log(TX_LOG_INFO, "tx_begin  xid=%ld", g_cur_xid);
    return TX_OK;
}

int tx_commit(void)
{
    if (g_cur_xid == 0) {
        tx_log(TX_LOG_WARN, "tx_commit: 트랜잭션 없음");
        return TX_FAIL;
    }
    tx_log(TX_LOG_INFO, "tx_commit xid=%ld", g_cur_xid);
    g_cur_xid = 0;
    return TX_OK;
}

int tx_abort(void)
{
    if (g_cur_xid == 0) {
        tx_log(TX_LOG_WARN, "tx_abort: 트랜잭션 없음");
        return TX_FAIL;
    }
    tx_log(TX_LOG_WARN, "tx_abort  xid=%ld", g_cur_xid);
    g_cur_xid = 0;
    return TX_OK;
}

long tx_current_xid(void) { return g_cur_xid; }
'''

MSG_LAYOUT_H = r'''/*
 * msg_layout.h - 카드 매입 전문(電文) 고정길이 레이아웃 (공유)
 */
#ifndef MSG_LAYOUT_H
#define MSG_LAYOUT_H

#define MSG_TYPE_RECV   "0200"
#define MSG_TYPE_RESP   "0210"
#define MSG_TYPE_RECON  "0500"

#define RESP_OK         "0000"
#define RESP_INVALID    "9001"
#define RESP_NOMERCH    "9002"
#define RESP_SYSERR     "9999"

typedef struct {
    char msg_type[4];
    char trans_code[6];
    char merch_id[15];
    char card_no[16];
    char approval_no[9];
    char amount[12];
    char txn_date[8];
    char txn_time[6];
    char inst_month[2];
    char resp_code[4];
    char filler[18];
} acq_msg_t;

#define MSG_ACQ_LEN   ((int)sizeof(acq_msg_t))

#define FLD_MSG_TYPE_LEN   4
#define FLD_TRANS_CODE_LEN 6
#define FLD_MERCH_ID_LEN   15
#define FLD_CARD_NO_LEN    16
#define FLD_APPROVAL_LEN   9
#define FLD_AMOUNT_LEN     12
#define FLD_TXN_DATE_LEN   8
#define FLD_TXN_TIME_LEN   6
#define FLD_INST_LEN       2
#define FLD_RESP_LEN       4

#endif /* MSG_LAYOUT_H */
'''

ACQ_UTIL_H = r'''/*
 * acq_util.h - 공통 유틸리티 (일자/금액/전문) 선언 (공유)
 */
#ifndef ACQ_UTIL_H
#define ACQ_UTIL_H

#include "msg_layout.h"

int  date_is_valid(const char *yyyymmdd);
int  date_weekday(const char *yyyymmdd);
int  date_is_weekend(const char *yyyymmdd);
int  date_next_business(const char *yyyymmdd, char *out);
void date_today(char *out);

long amount_parse(const char *field, int len);
int  amount_format(long amount, char *out, int len);
void amount_format_won(long amount, char *out, int outlen);
int  amount_is_valid(long amount);

void msg_field_copy(char *out, const char *field, int len);
void msg_field_set(char *field, const char *src, int len);
void msg_field_set_num(char *field, const char *src, int len);
int  msg_unpack(acq_msg_t *msg, const char *raw, int rawlen);
int  msg_pack(const acq_msg_t *msg, char *raw, int rawlen);
int  msg_validate(const acq_msg_t *msg);

#endif /* ACQ_UTIL_H */
'''

UTIL_DATE_C = r'''/*
 * util_date.c - 일자/영업일 유틸리티 (plain C, 공유)
 */
#include "acq_util.h"

#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include <time.h>

static int is_all_digit(const char *s, int len)
{
    int i;
    for (i = 0; i < len; i++)
        if (!isdigit((unsigned char)s[i]))
            return 0;
    return 1;
}

static int to_int(const char *s, int len)
{
    int i, v = 0;
    for (i = 0; i < len; i++)
        v = v * 10 + (s[i] - '0');
    return v;
}

static int days_in_month(int y, int m)
{
    static const int d[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    if (m < 1 || m > 12)
        return 0;
    if (m == 2) {
        int leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
        return leap ? 29 : 28;
    }
    return d[m - 1];
}

int date_is_valid(const char *yyyymmdd)
{
    int y, m, d;
    if (!yyyymmdd || strlen(yyyymmdd) < 8)
        return 0;
    if (!is_all_digit(yyyymmdd, 8))
        return 0;
    y = to_int(yyyymmdd, 4);
    m = to_int(yyyymmdd + 4, 2);
    d = to_int(yyyymmdd + 6, 2);
    if (y < 1900 || y > 2999) return 0;
    if (m < 1 || m > 12) return 0;
    if (d < 1 || d > days_in_month(y, m)) return 0;
    return 1;
}

int date_weekday(const char *yyyymmdd)
{
    static const int t[] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 };
    int y, m, d;
    if (!date_is_valid(yyyymmdd))
        return -1;
    y = to_int(yyyymmdd, 4);
    m = to_int(yyyymmdd + 4, 2);
    d = to_int(yyyymmdd + 6, 2);
    if (m < 3)
        y -= 1;
    return (y + y / 4 - y / 100 + y / 400 + t[m - 1] + d) % 7;
}

int date_is_weekend(const char *yyyymmdd)
{
    int w = date_weekday(yyyymmdd);
    return (w == 0 || w == 6) ? 1 : 0;
}

static void add_one_day(const char *in, char *out)
{
    int y = to_int(in, 4);
    int m = to_int(in + 4, 2);
    int d = to_int(in + 6, 2);
    d++;
    if (d > days_in_month(y, m)) {
        d = 1; m++;
        if (m > 12) { m = 1; y++; }
    }
    snprintf(out, 9, "%04d%02d%02d", y, m, d);
}

int date_next_business(const char *yyyymmdd, char *out)
{
    char cur[9];
    int guard = 0;
    if (!date_is_valid(yyyymmdd) || !out)
        return -1;
    memcpy(cur, yyyymmdd, 8);
    cur[8] = '\0';
    do {
        add_one_day(cur, cur);
        if (++guard > 14)
            return -1;
    } while (date_is_weekend(cur));
    memcpy(out, cur, 8);
    out[8] = '\0';
    return 0;
}

void date_today(char *out)
{
    time_t     now = time(NULL);
    struct tm  tmv;
    localtime_r(&now, &tmv);
    strftime(out, 9, "%Y%m%d", &tmv);
}
'''

UTIL_AMOUNT_C = r'''/*
 * util_amount.c - 금액/통화 유틸리티 (plain C, 공유)
 */
#include "acq_util.h"

#include <string.h>
#include <ctype.h>
#include <stdio.h>

#define AMOUNT_MAX  100000000L

long amount_parse(const char *field, int len)
{
    long v = 0;
    int  i;
    if (!field || len <= 0)
        return -1;
    for (i = 0; i < len; i++) {
        char c = field[i];
        if (c == ' ')
            continue;
        if (!isdigit((unsigned char)c))
            return -1;
        v = v * 10 + (c - '0');
    }
    return v;
}

int amount_format(long amount, char *out, int len)
{
    char tmp[32];
    int  n;
    if (!out || len <= 0 || amount < 0)
        return -1;
    n = snprintf(tmp, sizeof(tmp), "%0*ld", len, amount);
    if (n < 0 || n > len)
        return -1;
    memcpy(out, tmp, len);
    return 0;
}

void amount_format_won(long amount, char *out, int outlen)
{
    char raw[32];
    int  n, i, j, digits, first;
    if (!out || outlen <= 0)
        return;
    n = snprintf(raw, sizeof(raw), "%ld", amount);
    if (n < 0) { out[0] = '\0'; return; }
    first = (raw[0] == '-') ? 1 : 0;
    digits = n - first;
    j = 0;
    if (first && j < outlen - 1)
        out[j++] = '-';
    for (i = first; i < n && j < outlen - 1; i++) {
        int pos = i - first;
        if (pos > 0 && (digits - pos) % 3 == 0)
            if (j < outlen - 1)
                out[j++] = ',';
        out[j++] = raw[i];
    }
    out[j] = '\0';
}

int amount_is_valid(long amount)
{
    return (amount >= 1 && amount <= AMOUNT_MAX) ? 1 : 0;
}
'''

UTIL_MSG_C = r'''/*
 * util_msg.c - 고정길이 전문 pack/unpack 유틸리티 (plain C, 공유)
 */
#include "acq_util.h"

#include <string.h>
#include <ctype.h>

void msg_field_copy(char *out, const char *field, int len)
{
    int end;
    if (!out || !field || len < 0)
        return;
    end = len;
    while (end > 0 && (field[end - 1] == ' ' || field[end - 1] == '\0'))
        end--;
    memcpy(out, field, end);
    out[end] = '\0';
}

void msg_field_set(char *field, const char *src, int len)
{
    int slen, i;
    if (!field || len < 0)
        return;
    slen = src ? (int)strlen(src) : 0;
    if (slen > len)
        slen = len;
    memcpy(field, src, slen);
    for (i = slen; i < len; i++)
        field[i] = ' ';
}

void msg_field_set_num(char *field, const char *src, int len)
{
    int slen, pad, i;
    if (!field || len < 0)
        return;
    slen = src ? (int)strlen(src) : 0;
    if (slen > len)
        slen = len;
    pad = len - slen;
    for (i = 0; i < pad; i++)
        field[i] = '0';
    if (src)
        memcpy(field + pad, src, slen);
}

int msg_unpack(acq_msg_t *msg, const char *raw, int rawlen)
{
    if (!msg || !raw)
        return -1;
    if (rawlen < MSG_ACQ_LEN)
        return -1;
    memcpy(msg, raw, MSG_ACQ_LEN);
    return 0;
}

int msg_pack(const acq_msg_t *msg, char *raw, int rawlen)
{
    if (!msg || !raw)
        return -1;
    if (rawlen < MSG_ACQ_LEN)
        return -1;
    memcpy(raw, msg, MSG_ACQ_LEN);
    return 0;
}

static int field_blank(const char *field, int len)
{
    int i;
    for (i = 0; i < len; i++)
        if (field[i] != ' ' && field[i] != '\0')
            return 0;
    return 1;
}

int msg_validate(const acq_msg_t *msg)
{
    if (!msg)
        return -1;
    if (field_blank(msg->msg_type, FLD_MSG_TYPE_LEN)) return -1;
    if (field_blank(msg->merch_id, FLD_MERCH_ID_LEN)) return -1;
    if (field_blank(msg->card_no, FLD_CARD_NO_LEN)) return -1;
    if (field_blank(msg->amount, FLD_AMOUNT_LEN)) return -1;
    if (field_blank(msg->txn_date, FLD_TXN_DATE_LEN)) return -1;
    return 0;
}
'''

# ======================================================================
# 모듈 core 헤더 (표준 DBIO API + 레코드 구조 + 상태코드)
# ======================================================================
CORE_H_TPL = r'''/*
 * @mod@_core.h - @KMOD@ 모듈 핵심 계약 (레코드/상태/표준 DBIO API)
 *
 * 이 모듈의 온라인/배치 프로그램은 SQL 을 직접 다루지 않고 아래 표준
 * DBIO API 만 호출한다. 구현은 app/dbio/@mod@_dbio_main.pgc (ECPG).
 */
#ifndef @MOD@_CORE_H
#define @MOD@_CORE_H

#include "txcore.h"

/* @KMOD@ 처리 상태코드 */
#define @MOD@_ST_RECV    "R"   /* 접수/수신   */
#define @MOD@_ST_DONE    "M"   /* 처리완료    */
#define @MOD@_ST_FAIL    "U"   /* 불일치/실패 */
#define @MOD@_ST_SETTLED "S"   /* 정산완료    */

/* @KMOD@ 원장 레코드 (1행) */
typedef struct {
    char key[16];        /* 처리키(승인/거래 번호) PK */
    char merch_id[16];   /* 가맹점번호        */
    char card_no[20];    /* 카드번호(마스킹)  */
    long amount;         /* 거래금액(원)      */
    char biz_date[9];    /* 영업일 YYYYMMDD   */
    char status[2];      /* 상태코드          */
    long fee;            /* 수수료(원)        */
} @mod@_rec_t;

/* 표준 DBIO API (구현: @mod@_dbio_main.pgc) */
int  @mod@_dbio_connect(const char *target);
int  @mod@_dbio_disconnect(void);
int  @mod@_rec_insert(const @mod@_rec_t *rec);
int  @mod@_rec_select(const char *key, @mod@_rec_t *out);
int  @mod@_rec_update_status(const char *key, const char *status);
long @mod@_rec_count(const char *biz_date, const char *status);
int  @mod@_rec_sum(const char *biz_date, long *out_sum);

#endif /* @MOD@_CORE_H */
'''

DBIO_MAIN_TPL = r'''/* @tier medium @module @mod@ @transform @XFORM@-dbio */
/*
 * @mod@_dbio_main.pgc - @KMOD@ 표준 DBIO (ECPG 임베디드 SQL)
 *
 * @mod@_core.h 의 표준 API 를 @mod@_ledger 테이블에 대해 구현한다.
 * 온라인/배치 프로그램은 이 함수들만 호출한다 (self-contained).
 */
#include <stdio.h>
#include <string.h>

#include "txcore.h"
#include "txcore_dbio.h"
#include "@mod@_core.h"

EXEC SQL INCLUDE sqlca;

int @mod@_dbio_connect(const char *target)
{
    EXEC SQL BEGIN DECLARE SECTION;
    const char *h_target;
    EXEC SQL END DECLARE SECTION;

    h_target = (target && target[0]) ? target : "acquire@localhost";

    EXEC SQL CONNECT TO :h_target;
    if (sqlca.sqlcode != TX_SQL_OK) {
        TX_DBIO_LOG_ERR("@mod@ CONNECT");
        return TX_EDB;
    }
    tx_log(TX_LOG_INFO, "[@mod@] DB 접속 완료 target=%s", h_target);
    return TX_OK;
}

int @mod@_dbio_disconnect(void)
{
    EXEC SQL DISCONNECT CURRENT;
    return TX_OK;
}

int @mod@_rec_insert(const @mod@_rec_t *rec)
{
    EXEC SQL BEGIN DECLARE SECTION;
    char h_key[16];
    char h_merch_id[16];
    char h_card_no[20];
    long h_amount;
    char h_biz_date[9];
    char h_status[2];
    long h_fee;
    EXEC SQL END DECLARE SECTION;

    if (!rec)
        return TX_EINVAL;

    strncpy(h_key, rec->key, sizeof(h_key) - 1);
    h_key[sizeof(h_key) - 1] = '\0';
    strncpy(h_merch_id, rec->merch_id, sizeof(h_merch_id) - 1);
    h_merch_id[sizeof(h_merch_id) - 1] = '\0';
    strncpy(h_card_no, rec->card_no, sizeof(h_card_no) - 1);
    h_card_no[sizeof(h_card_no) - 1] = '\0';
    strncpy(h_biz_date, rec->biz_date, sizeof(h_biz_date) - 1);
    h_biz_date[sizeof(h_biz_date) - 1] = '\0';
    strncpy(h_status, rec->status, sizeof(h_status) - 1);
    h_status[sizeof(h_status) - 1] = '\0';
    h_amount = rec->amount;
    h_fee    = rec->fee;

    EXEC SQL INSERT INTO @mod@_ledger
        (key, merch_id, card_no, amount, biz_date, status, fee)
        VALUES
        (:h_key, :h_merch_id, :h_card_no, :h_amount, :h_biz_date, :h_status, :h_fee);

    if (sqlca.sqlcode != TX_SQL_OK) {
        TX_DBIO_LOG_ERR("@mod@_rec_insert");
        return TX_EDB;
    }
    return TX_OK;
}

int @mod@_rec_select(const char *key, @mod@_rec_t *out)
{
    EXEC SQL BEGIN DECLARE SECTION;
    char h_key[16];
    char h_merch_id[16];
    char h_card_no[20];
    long h_amount;
    char h_biz_date[9];
    char h_status[2];
    long h_fee;
    EXEC SQL END DECLARE SECTION;

    if (!key || !out)
        return TX_EINVAL;

    strncpy(h_key, key, sizeof(h_key) - 1);
    h_key[sizeof(h_key) - 1] = '\0';

    EXEC SQL SELECT merch_id, card_no, amount, biz_date, status, fee
        INTO :h_merch_id, :h_card_no, :h_amount, :h_biz_date, :h_status, :h_fee
        FROM @mod@_ledger
        WHERE key = :h_key;

    if (sqlca.sqlcode == TX_SQL_NOTFOUND)
        return TX_ENOENT;
    if (sqlca.sqlcode != TX_SQL_OK) {
        TX_DBIO_LOG_ERR("@mod@_rec_select");
        return TX_EDB;
    }

    memset(out, 0, sizeof(*out));
    strncpy(out->key, h_key, sizeof(out->key) - 1);
    strncpy(out->merch_id, h_merch_id, sizeof(out->merch_id) - 1);
    strncpy(out->card_no, h_card_no, sizeof(out->card_no) - 1);
    strncpy(out->biz_date, h_biz_date, sizeof(out->biz_date) - 1);
    strncpy(out->status, h_status, sizeof(out->status) - 1);
    out->amount = h_amount;
    out->fee    = h_fee;
    return TX_OK;
}

int @mod@_rec_update_status(const char *key, const char *status)
{
    EXEC SQL BEGIN DECLARE SECTION;
    char h_key[16];
    char h_status[2];
    EXEC SQL END DECLARE SECTION;

    if (!key || !status)
        return TX_EINVAL;

    strncpy(h_key, key, sizeof(h_key) - 1);
    h_key[sizeof(h_key) - 1] = '\0';
    strncpy(h_status, status, sizeof(h_status) - 1);
    h_status[sizeof(h_status) - 1] = '\0';

    EXEC SQL UPDATE @mod@_ledger
        SET status = :h_status, upd_ts = now()
        WHERE key = :h_key;

    if (sqlca.sqlcode != TX_SQL_OK) {
        TX_DBIO_LOG_ERR("@mod@_rec_update_status");
        return TX_EDB;
    }
    if (sqlca.sqlerrd[2] == 0)
        return TX_ENOENT;
    return TX_OK;
}

long @mod@_rec_count(const char *biz_date, const char *status)
{
    EXEC SQL BEGIN DECLARE SECTION;
    char h_biz_date[9];
    char h_status[2];
    int  h_cnt;
    EXEC SQL END DECLARE SECTION;

    if (!biz_date || !status)
        return -1;

    strncpy(h_biz_date, biz_date, sizeof(h_biz_date) - 1);
    h_biz_date[sizeof(h_biz_date) - 1] = '\0';
    strncpy(h_status, status, sizeof(h_status) - 1);
    h_status[sizeof(h_status) - 1] = '\0';

    EXEC SQL SELECT count(*)
        INTO :h_cnt
        FROM @mod@_ledger
        WHERE biz_date = :h_biz_date
          AND status   = :h_status;

    if (sqlca.sqlcode != TX_SQL_OK) {
        TX_DBIO_LOG_ERR("@mod@_rec_count");
        return -1;
    }
    return (long)h_cnt;
}

int @mod@_rec_sum(const char *biz_date, long *out_sum)
{
    EXEC SQL BEGIN DECLARE SECTION;
    char h_biz_date[9];
    long h_sum;
    EXEC SQL END DECLARE SECTION;

    if (!biz_date || !out_sum)
        return TX_EINVAL;

    strncpy(h_biz_date, biz_date, sizeof(h_biz_date) - 1);
    h_biz_date[sizeof(h_biz_date) - 1] = '\0';

    EXEC SQL SELECT coalesce(sum(amount), 0)
        INTO :h_sum
        FROM @mod@_ledger
        WHERE biz_date = :h_biz_date;

    if (sqlca.sqlcode != TX_SQL_OK) {
        TX_DBIO_LOG_ERR("@mod@_rec_sum");
        return TX_EDB;
    }
    *out_sum = h_sum;
    return TX_OK;
}
'''

# ======================================================================
# 온라인 서비스 (.pgc)  - easy / medium / hard
# ======================================================================
ONLINE_HEAD = r'''/* @tier @TIER@ @module @mod@ @transform @XFORM@-online */
/*
 * @ID@.pgc - @KMOD@ 온라인 서비스 (@SVC@)  [tier=@TIER@]
 *
 * TxCore TX_SERVICE 엔트리. 요청 TXBUF 를 검증하고 @mod@ 표준 DBIO 를
 * 통해 처리한 뒤 응답 TXBUF 를 구성한다. (ATMI void SVC(TPSVCINFO*) 대응)
 */
#include <stdio.h>
#include <string.h>

#include "txcore.h"
#include "acq_util.h"
#include "@mod@_core.h"

EXEC SQL INCLUDE sqlca;

'''

ONLINE_EASY = r'''TX_SERVICE(@SVC@, ctx)
{
    @mod@_rec_t rec;
    char key[16];
    int  rc;

    tx_log(TX_LOG_INFO, "[@SVC@] @KMOD@ 단건조회 서비스 진입");

    if (tx_buf_get(ctx->in, "KEY", key, sizeof(key)) != TX_OK) {
        tx_log(TX_LOG_ERROR, "[@SVC@] KEY 누락");
        tx_return(ctx, TX_EINVAL, ctx->out);
        return;
    }

    memset(&rec, 0, sizeof(rec));
    rc = @mod@_rec_select(key, &rec);
    if (rc != TX_OK) {
        tx_log(TX_LOG_WARN, "[@SVC@] 조회 실패 key=%s rc=%d(%s)",
               key, rc, tx_strerror(rc));
        tx_return(ctx, rc, ctx->out);
        return;
    }

    tx_buf_reset(ctx->out);
    tx_buf_sets(ctx->out, "KEY", rec.key);
    tx_buf_sets(ctx->out, "MERCHID", rec.merch_id);
    tx_buf_setlong(ctx->out, "AMOUNT", rec.amount);
    tx_buf_sets(ctx->out, "STATUS", rec.status);
    tx_log(TX_LOG_INFO, "[@SVC@] 조회완료 key=%s amount=%ld", key, rec.amount);
    tx_return(ctx, TX_OK, ctx->out);
}
'''

ONLINE_MEDIUM = r'''TX_SERVICE(@SVC@, ctx)
{
    @mod@_rec_t rec;
    char key[16], merch[16], bizdate[16];
    long amount = 0, cnt;
    int  rc;

    tx_log(TX_LOG_INFO, "[@SVC@] @KMOD@ 등록 서비스 진입");

    if (tx_buf_get(ctx->in, "KEY", key, sizeof(key)) != TX_OK ||
        tx_buf_get(ctx->in, "MERCHID", merch, sizeof(merch)) != TX_OK ||
        tx_buf_get(ctx->in, "BIZDATE", bizdate, sizeof(bizdate)) != TX_OK) {
        tx_log(TX_LOG_ERROR, "[@SVC@] 필수 필드 누락");
        tx_return(ctx, TX_EINVAL, ctx->out);
        return;
    }
    tx_buf_getlong(ctx->in, "AMOUNT", &amount);

    /* 업무검증: 금액 범위 + 영업일 형식 */
    if (!amount_is_valid(amount)) {
        tx_log(TX_LOG_WARN, "[@SVC@] 금액 범위 오류 amount=%ld", amount);
        tx_return(ctx, TX_EINVAL, ctx->out);
        return;
    }
    if (!date_is_valid(bizdate)) {
        tx_log(TX_LOG_WARN, "[@SVC@] 영업일 오류 date=%s", bizdate);
        tx_return(ctx, TX_EINVAL, ctx->out);
        return;
    }

    memset(&rec, 0, sizeof(rec));
    strncpy(rec.key, key, sizeof(rec.key) - 1);
    strncpy(rec.merch_id, merch, sizeof(rec.merch_id) - 1);
    strncpy(rec.biz_date, bizdate, sizeof(rec.biz_date) - 1);
    strncpy(rec.status, @MOD@_ST_RECV, sizeof(rec.status) - 1);
    rec.amount = amount;
    rec.fee    = amount / 100;   /* 수수료 1% 가정 */

    rc = @mod@_rec_insert(&rec);
    if (rc != TX_OK) {
        tx_log(TX_LOG_ERROR, "[@SVC@] 등록 실패 rc=%d(%s)", rc, tx_strerror(rc));
        tx_return(ctx, rc, ctx->out);
        return;
    }

    cnt = @mod@_rec_count(bizdate, @MOD@_ST_RECV);

    tx_buf_reset(ctx->out);
    tx_buf_sets(ctx->out, "RESPCODE", RESP_OK);
    tx_buf_sets(ctx->out, "KEY", key);
    tx_buf_setlong(ctx->out, "DAYCNT", (cnt < 0) ? 0 : cnt);
    tx_log(TX_LOG_INFO, "[@SVC@] 등록완료 key=%s 당일건수=%ld", key, cnt);
    tx_return(ctx, TX_OK, ctx->out);
}
'''

ONLINE_HARD = r'''TX_SERVICE(@SVC@, ctx)
{
    @mod@_rec_t rec;
    char key[16], merch[16], bizdate[16], nextbiz[9];
    long amount = 0, daysum = 0;
    int  rc;
    TXBUF *sub_in;
    TXBUF *sub_out;

    tx_log(TX_LOG_INFO, "[@SVC@] @KMOD@ 트랜잭션 오케스트레이션 진입");

    if (tx_buf_get(ctx->in, "KEY", key, sizeof(key)) != TX_OK ||
        tx_buf_get(ctx->in, "MERCHID", merch, sizeof(merch)) != TX_OK ||
        tx_buf_get(ctx->in, "BIZDATE", bizdate, sizeof(bizdate)) != TX_OK) {
        tx_log(TX_LOG_ERROR, "[@SVC@] 필수 필드 누락");
        tx_return(ctx, TX_EINVAL, ctx->out);
        return;
    }
    tx_buf_getlong(ctx->in, "AMOUNT", &amount);

    if (!amount_is_valid(amount) || !date_is_valid(bizdate)) {
        tx_log(TX_LOG_WARN, "[@SVC@] 입력 검증 실패 amount=%ld date=%s",
               amount, bizdate);
        tx_return(ctx, TX_EINVAL, ctx->out);
        return;
    }

    /* 트랜잭션 시작 → 원장 반영 → 후속 서비스 연계 → 커밋 */
    if (tx_begin() != TX_OK) {
        tx_return(ctx, TX_FAIL, ctx->out);
        return;
    }

    memset(&rec, 0, sizeof(rec));
    strncpy(rec.key, key, sizeof(rec.key) - 1);
    strncpy(rec.merch_id, merch, sizeof(rec.merch_id) - 1);
    strncpy(rec.biz_date, bizdate, sizeof(rec.biz_date) - 1);
    strncpy(rec.status, @MOD@_ST_RECV, sizeof(rec.status) - 1);
    rec.amount = amount;
    rec.fee    = amount / 100;

    rc = @mod@_rec_insert(&rec);
    if (rc != TX_OK) {
        tx_log(TX_LOG_ERROR, "[@SVC@] 원장반영 실패 rc=%d", rc);
        tx_abort();
        tx_return(ctx, rc, ctx->out);
        return;
    }

    /* 정산 영업일 = 거래일의 다음 영업일 */
    if (date_next_business(bizdate, nextbiz) != 0) {
        memcpy(nextbiz, bizdate, 8);
        nextbiz[8] = '\0';
    }

    /* 후속 정산집계 서비스 연계 (레지스트리 문자열 디스패치) */
    sub_in  = tx_buf_alloc();
    sub_out = tx_buf_alloc();
    if (sub_in && sub_out) {
        tx_buf_sets(sub_in, "MERCHID", merch);
        tx_buf_sets(sub_in, "BIZDATE", nextbiz);
        tx_buf_setlong(sub_in, "AMOUNT", amount);
        rc = tx_call("@NEXTSVC@", sub_in, sub_out);
        if (rc != TX_OK)
            tx_log(TX_LOG_WARN, "[@SVC@] 후속연계 경고 rc=%d", rc);
    }
    tx_buf_free(sub_in);
    tx_buf_free(sub_out);

    /* 교차 검증: 당일 합계 재계산 */
    if (@mod@_rec_sum(bizdate, &daysum) != TX_OK)
        daysum = 0;

    tx_commit();

    tx_buf_reset(ctx->out);
    tx_buf_sets(ctx->out, "RESPCODE", RESP_OK);
    tx_buf_sets(ctx->out, "KEY", key);
    tx_buf_setlong(ctx->out, "DAYSUM", daysum);
    tx_log(TX_LOG_INFO, "[@SVC@] 완료 key=%s 당일합계=%ld", key, daysum);
    tx_return(ctx, TX_OK, ctx->out);
}
'''

# ======================================================================
# 배치 (.pgc)  - 커서 DECLARE/OPEN/FETCH 루프 + main()
# ======================================================================
BATCH_TPL = r'''/* @tier @TIER@ @module @mod@ @transform @XFORM@-batch */
/*
 * @ID@.pgc - @KMOD@ 배치 (커서 순회 + 상태전이)  [tier=@TIER@]
 *
 * 지정 영업일의 접수(status='R') 건을 커서로 순회하며 업무규칙에 따라
 * 완료('M')/불일치('U') 로 상태를 전이한다. 배치 main 엔트리 포함.
 *
 * 실행:  @ID@ <YYYYMMDD> [db@host]
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "txcore.h"
#include "txcore_dbio.h"
#include "acq_util.h"
#include "@mod@_core.h"

EXEC SQL INCLUDE sqlca;

static int run_@ID@(const char *biz_date)
{
    EXEC SQL BEGIN DECLARE SECTION;
    char h_biz_date[9];
    char h_key[16];
    char h_merch_id[16];
    long h_amount;
    char h_new_status[2];
    EXEC SQL END DECLARE SECTION;

    long total = 0, done = 0, unmatch = 0, errcnt = 0;
    int  rc;

    strncpy(h_biz_date, biz_date, sizeof(h_biz_date) - 1);
    h_biz_date[sizeof(h_biz_date) - 1] = '\0';

    EXEC SQL DECLARE cur_@ID@ CURSOR FOR
        SELECT key, merch_id, amount
        FROM @mod@_ledger
        WHERE biz_date = :h_biz_date
          AND status   = 'R'
        ORDER BY key;

    EXEC SQL OPEN cur_@ID@;
    if (sqlca.sqlcode != TX_SQL_OK) {
        TX_DBIO_LOG_ERR("OPEN cur_@ID@");
        return TX_EDB;
    }

    if (tx_begin() != TX_OK)
        tx_log(TX_LOG_WARN, "[@ID@] tx_begin 경고");

    for (;;) {
        EXEC SQL FETCH cur_@ID@ INTO :h_key, :h_merch_id, :h_amount;

        if (sqlca.sqlcode == TX_SQL_NOTFOUND)
            break;
        if (sqlca.sqlcode != TX_SQL_OK) {
            TX_DBIO_LOG_ERR("FETCH cur_@ID@");
            errcnt++;
            break;
        }

        total++;

        /* 업무규칙: 금액 유효성으로 완료/불일치 판정 */
        if (amount_is_valid(h_amount)) {
            strncpy(h_new_status, @MOD@_ST_DONE, sizeof(h_new_status));
            done++;
        } else {
            strncpy(h_new_status, @MOD@_ST_FAIL, sizeof(h_new_status));
            unmatch++;
        }

        rc = @mod@_rec_update_status(h_key, h_new_status);
        if (rc != TX_OK) {
            tx_log(TX_LOG_ERROR, "[@ID@] 상태전이 실패 key=%s rc=%d", h_key, rc);
            errcnt++;
        }
    }

    EXEC SQL CLOSE cur_@ID@;

    if (errcnt == 0)
        tx_commit();
    else
        tx_abort();

    tx_log(TX_LOG_INFO,
           "[@ID@] 요약 date=%s 대상=%ld 완료=%ld 불일치=%ld 오류=%ld",
           biz_date, total, done, unmatch, errcnt);

    printf("========== @KMOD@ 배치 결과 [@ID@] ==========\n");
    printf("  영업일      : %s\n", biz_date);
    printf("  대상 건수   : %ld\n", total);
    printf("  완료(M)     : %ld\n", done);
    printf("  불일치(U)   : %ld\n", unmatch);
    printf("  오류 건수   : %ld\n", errcnt);
    printf("=============================================\n");

    return (errcnt == 0) ? TX_OK : TX_FAIL;
}

int main(int argc, char **argv)
{
    const char *biz_date;
    const char *target;
    int rc;

    tx_set_loglevel(TX_LOG_INFO);

    if (argc < 2) {
        fprintf(stderr, "사용법: %s <YYYYMMDD> [db@host]\n", argv[0]);
        return 2;
    }
    biz_date = argv[1];
    target   = (argc >= 3) ? argv[2] : NULL;

    if (!date_is_valid(biz_date)) {
        fprintf(stderr, "오류: 유효하지 않은 영업일 '%s'\n", biz_date);
        return 2;
    }

    tx_log(TX_LOG_INFO, "[@ID@] 배치 시작 date=%s", biz_date);

    rc = @mod@_dbio_connect(target);
    if (rc != TX_OK) {
        tx_log(TX_LOG_ERROR, "[@ID@] DB 접속 실패 rc=%d", rc);
        return 1;
    }

    rc = run_@ID@(biz_date);

    @mod@_dbio_disconnect();

    tx_log(TX_LOG_INFO, "[@ID@] 배치 종료 rc=%d(%s)", rc, tx_strerror(rc));
    return (rc == TX_OK) ? 0 : 1;
}
'''

# ======================================================================
# 추가 DBIO (.pgc) - 자체 헤더에 선언된 함수 구현 (self-contained)
# ======================================================================
DBIO_EXTRA_H = r'''/*
 * @ID@.h - @KMOD@ 보조 DBIO 선언 (@ID@.pgc 구현)
 */
#ifndef @IDU@_H
#define @IDU@_H

#include "txcore.h"

/* @ID@ 전용 조회/갱신 (테이블 @ID@_t) */
int  @ID@_fetch(const char *key, long *out_amount);
int  @ID@_touch(const char *key, long amount);
long @ID@_total(const char *biz_date);

#endif /* @IDU@_H */
'''

DBIO_EXTRA_TPL = r'''/* @tier @TIER@ @module @mod@ @transform @XFORM@-dbio */
/*
 * @ID@.pgc - @KMOD@ 보조 DBIO (ECPG)  [tier=@TIER@]
 */
#include <stdio.h>
#include <string.h>

#include "txcore.h"
#include "txcore_dbio.h"
#include "@ID@.h"

EXEC SQL INCLUDE sqlca;

int @ID@_fetch(const char *key, long *out_amount)
{
    EXEC SQL BEGIN DECLARE SECTION;
    char h_key[16];
    long h_amount;
    EXEC SQL END DECLARE SECTION;

    if (!key || !out_amount)
        return TX_EINVAL;

    strncpy(h_key, key, sizeof(h_key) - 1);
    h_key[sizeof(h_key) - 1] = '\0';

    EXEC SQL SELECT amount
        INTO :h_amount
        FROM @ID@_t
        WHERE key = :h_key;

    if (sqlca.sqlcode == TX_SQL_NOTFOUND)
        return TX_ENOENT;
    if (sqlca.sqlcode != TX_SQL_OK) {
        TX_DBIO_LOG_ERR("@ID@_fetch");
        return TX_EDB;
    }
    *out_amount = h_amount;
    return TX_OK;
}

int @ID@_touch(const char *key, long amount)
{
    EXEC SQL BEGIN DECLARE SECTION;
    char h_key[16];
    long h_amount;
    EXEC SQL END DECLARE SECTION;

    if (!key)
        return TX_EINVAL;

    strncpy(h_key, key, sizeof(h_key) - 1);
    h_key[sizeof(h_key) - 1] = '\0';
    h_amount = amount;

    EXEC SQL UPDATE @ID@_t
        SET amount = :h_amount, upd_ts = now()
        WHERE key = :h_key;

    if (sqlca.sqlcode != TX_SQL_OK) {
        TX_DBIO_LOG_ERR("@ID@_touch");
        return TX_EDB;
    }
    if (sqlca.sqlerrd[2] == 0)
        return TX_ENOENT;
    return TX_OK;
}

long @ID@_total(const char *biz_date)
{
    EXEC SQL BEGIN DECLARE SECTION;
    char h_biz_date[9];
    long h_sum;
    EXEC SQL END DECLARE SECTION;

    if (!biz_date)
        return -1;

    strncpy(h_biz_date, biz_date, sizeof(h_biz_date) - 1);
    h_biz_date[sizeof(h_biz_date) - 1] = '\0';

    EXEC SQL SELECT coalesce(sum(amount), 0)
        INTO :h_sum
        FROM @ID@_t
        WHERE biz_date = :h_biz_date;

    if (sqlca.sqlcode != TX_SQL_OK) {
        TX_DBIO_LOG_ERR("@ID@_total");
        return -1;
    }
    return h_sum;
}
'''

# ======================================================================
# 공통 유틸 (.c) - plain C
# ======================================================================
COMMON_H = r'''/*
 * @ID@.h - @KMOD@ 공통 유틸 선언 (@ID@.c 구현)
 */
#ifndef @IDU@_H
#define @IDU@_H

long @ID@_fee(long amount, int rate_bp);
long @ID@_round_unit(long amount, long unit);
int  @ID@_classify(long amount);

#endif /* @IDU@_H */
'''

COMMON_TPL = r'''/* @tier @TIER@ @module @mod@ @transform @XFORM@-util */
/*
 * @ID@.c - @KMOD@ 공통 유틸리티 (plain C)  [tier=@TIER@]
 */
#include "@ID@.h"

#include <string.h>

/* 수수료 계산: rate_bp 는 basis point(1bp=0.01%) */
long @ID@_fee(long amount, int rate_bp)
{
    long fee;
    if (amount <= 0 || rate_bp < 0)
        return 0;
    fee = (amount * (long)rate_bp) / 10000L;
    if (fee < 0)
        fee = 0;
    return fee;
}

/* 절사(내림) 단위 정규화 */
long @ID@_round_unit(long amount, long unit)
{
    if (unit <= 0)
        return amount;
    return (amount / unit) * unit;
}

/* 금액 구간 분류: 0=소액 1=일반 2=고액 */
int @ID@_classify(long amount)
{
    if (amount < 10000L)
        return 0;
    if (amount < 1000000L)
        return 1;
    return 2;
}
'''

# ======================================================================
# 독립 헤더 (전문 struct / 상수 / 레코드) - 4종 로테이션
# ======================================================================
HDR_REC = r'''/*
 * @ID@.h - @KMOD@ 레코드/뷰 구조 정의
 */
#ifndef @IDU@_H
#define @IDU@_H

/* @KMOD@ 처리 뷰 레코드 */
typedef struct {
    char  key[16];
    char  merch_id[16];
    long  amount;
    long  fee;
    char  biz_date[9];
    char  status[2];
    char  reserved[16];
} @ID@_view_t;

#define @IDU@_STATUS_OPEN  "O"
#define @IDU@_STATUS_HOLD  "H"
#define @IDU@_STATUS_CLOSE "C"

#endif /* @IDU@_H */
'''

HDR_MSG = r'''/*
 * @ID@.h - @KMOD@ 고정길이 전문 레이아웃 (positional, 재배치 금지)
 */
#ifndef @IDU@_H
#define @IDU@_H

typedef struct {
    char msg_type[4];    /* 전문구분   4  */
    char merch_id[15];   /* 가맹점번호 15 */
    char key[9];         /* 처리키     9  */
    char amount[12];     /* 금액       12 zero-pad */
    char biz_date[8];    /* 영업일     8  */
    char resp_code[4];   /* 응답코드   4  */
    char filler[20];     /* 예비       20 */
} @ID@_msg_t;

#define @IDU@_MSG_LEN   ((int)sizeof(@ID@_msg_t))

#endif /* @IDU@_H */
'''

HDR_CONST = r'''/*
 * @ID@.h - @KMOD@ 코드/상수 테이블
 */
#ifndef @IDU@_H
#define @IDU@_H

#define @IDU@_RC_OK       0
#define @IDU@_RC_RETRY    1
#define @IDU@_RC_SKIP     2
#define @IDU@_RC_ERROR    9

#define @IDU@_LIMIT_DAILY  100000000L
#define @IDU@_LIMIT_SINGLE  50000000L
#define @IDU@_FEE_RATE_BP        250   /* 2.50% */

#define @IDU@_CHAN_ONLINE  "01"
#define @IDU@_CHAN_BATCH   "02"
#define @IDU@_CHAN_RECON   "03"

#endif /* @IDU@_H */
'''

HDR_DECL = r'''/*
 * @ID@.h - @KMOD@ 내부 헬퍼 선언
 */
#ifndef @IDU@_H
#define @IDU@_H

#include "txcore.h"

int  @ID@_check(const char *key, long amount);
int  @ID@_apply(TXBUF *in, TXBUF *out);
long @ID@_eval(long amount, int factor);

#endif /* @IDU@_H */
'''

HDR_ROTATION = [HDR_REC, HDR_MSG, HDR_CONST, HDR_DECL]

# ======================================================================
# 데모 서버 main (온라인 데모 링크용, 생성 산출물 검증)
# ======================================================================
DEMO_MAIN_TPL = r'''/*
 * demo_@mod@_main.c - @KMOD@ 온라인 데모 부트스트랩 (링크 검증)
 *
 * 생성된 플래그십 서비스(@SVC@)를 TxCore 레지스트리에 등록하여
 * 등록/디스패치/DBIO 링크 경로가 실제로 연결됨을 증명한다.
 */
#include <stdio.h>

#include "txcore.h"

void @SVC@(TXSVCINFO *ctx);

int main(void)
{
    tx_set_loglevel(TX_LOG_INFO);
    tx_log(TX_LOG_INFO, "@KMOD@ 온라인 데모 기동 (TxCore)");

    tx_register("@SVC@", @SVC@);

    tx_log(TX_LOG_INFO, "등록 서비스 수 = %d", tx_service_count());
    printf("demo_@mod@_server 준비완료: 서비스 %d개 등록\n",
           tx_service_count());
    return 0;
}
'''

# ======================================================================
# 비컴파일 아티팩트 템플릿
# ======================================================================
FML_TPL = '''#
# @ID@.fml - @KMOD@ FML 필드 테이블 (UBF 필드 정의)
# *base 1@BASE@0
#
# name          rel-number   type    flags   comments
@ID@_KEY        1            string   -       처리키
@ID@_MERCHID    2            string   -       가맹점번호
@ID@_CARDNO     3            string   -       카드번호(마스킹)
@ID@_AMOUNT     4            long     -       거래금액
@ID@_BIZDATE    5            string   -       영업일
@ID@_STATUS     6            char     -       상태코드
@ID@_FEE        7            long     -       수수료
@ID@_RESPCODE   8            string   -       응답코드
'''

SH_TPL = '''#!/bin/sh
# @ID@.sh - @KMOD@ 실행 러너 (@TIER@)
# 사용법: @ID@.sh <YYYYMMDD>
set -eu

BIZDATE="${1:-$(date +%Y%m%d)}"
APP_HOME="${APP_HOME:-/app/acquire-core}"
BIN="${APP_HOME}/bin"
LOG="${APP_HOME}/log/@ID@_${BIZDATE}.log"

echo "[@ID@] @KMOD@ 처리 시작 date=${BIZDATE}"
if [ -x "${BIN}/@RUNBIN@" ]; then
    "${BIN}/@RUNBIN@" "${BIZDATE}" "acquire@localhost" >> "${LOG}" 2>&1
    RC=$?
else
    echo "[@ID@] 실행 파일 없음: ${BIN}/@RUNBIN@" >&2
    RC=0
fi
echo "[@ID@] 종료 rc=${RC}"
exit ${RC}
'''

MK_TPL = '''# @ID@.mk - @KMOD@ 빌드 조각 (참고용 include 단편)
# 이 조각은 상위 Makefile 이 아카이브/배포 시 참조하는 메타데이터다.
@IDU@_MODULE   := @mod@
@IDU@_TIER     := @TIER@
@IDU@_SRC      := app/@SUBDIR@/@REF@
@IDU@_DESC     := @KMOD@ @XFORM@ 처리 단위

.PHONY: @ID@-info
@ID@-info:
	@echo "@ID@ module=@mod@ tier=@TIER@ transform=@XFORM@"
'''

UBB_TPL = '''#
# @ID@.ubb - @KMOD@ TP 도메인 구성 (Tuxedo UBBCONFIG 형식, 참고)
#
*RESOURCES
    IPCKEY      @IPCKEY@
    MASTER      SITE1
    MODEL       SHM
    LDBAL       Y

*MACHINES
    "acqhost"   LMID=SITE1
                APPDIR="/app/acquire-core"
                TUXCONFIG="/app/acquire-core/tuxconfig"

*GROUPS
    GRP_@MOD@   LMID=SITE1  GRPNO=@GRPNO@

*SERVERS
    @MOD@_SVR   SRVGRP=GRP_@MOD@  SRVID=@GRPNO@  CLOPT="-A"

*SERVICES
    @MOD@_SVC   LOAD=50  PRIO=50
'''

CFG_TPL = '''# @ID@.cfg - @KMOD@ 런타임 설정 (@TIER@)
[general]
module      = @mod@
transform   = @XFORM@
tier        = @TIER@

[db]
dsn         = acquire@localhost
schema      = public
fetch_size  = 1000

[limits]
daily_cap   = 100000000
single_cap  = 50000000
fee_rate_bp = 250
'''

DAT_TPL = '''# @ID@.dat - @KMOD@ 코드 매핑 데이터 (파이프 구분)
# code|label|value
01|@KMOD@ 접수|R
02|@KMOD@ 완료|M
03|@KMOD@ 불일치|U
04|@KMOD@ 정산|S
'''

SQL_TPL = '''-- @ID@.sql - @KMOD@ DBIO 스키마/뷰 정의 (@TIER@)
-- 런타임 PostgreSQL 에 적용. 컴파일에는 불필요.

CREATE TABLE IF NOT EXISTS @mod@_ledger (
    key       VARCHAR(15)  NOT NULL,
    merch_id  VARCHAR(15)  NOT NULL,
    card_no   VARCHAR(19)  NOT NULL,
    amount    BIGINT       NOT NULL DEFAULT 0,
    biz_date  CHAR(8)      NOT NULL,
    status    CHAR(1)      NOT NULL DEFAULT 'R',
    fee       BIGINT       NOT NULL DEFAULT 0,
    reg_ts    TIMESTAMP    NOT NULL DEFAULT now(),
    upd_ts    TIMESTAMP,
    CONSTRAINT pk_@mod@_ledger PRIMARY KEY (key)
);

CREATE INDEX IF NOT EXISTS ix_@ID@_date_status
    ON @mod@_ledger (biz_date, status);

CREATE OR REPLACE VIEW @ID@_daily_v AS
    SELECT biz_date, status, count(*) AS cnt, sum(amount) AS total
    FROM @mod@_ledger
    GROUP BY biz_date, status;
'''

SCHEMA_SQL = '''-- schema.sql - acquire-core-full 공통 스키마 루트
-- 모듈별 원장 테이블은 db/<mod>_*.sql 참조.

CREATE TABLE IF NOT EXISTS merchant (
    merch_id   VARCHAR(15) NOT NULL,
    merch_name VARCHAR(60) NOT NULL,
    biz_no     VARCHAR(12),
    status     CHAR(1)     NOT NULL DEFAULT 'A',
    CONSTRAINT pk_merchant PRIMARY KEY (merch_id)
);

CREATE TABLE IF NOT EXISTS approval (
    appr_no   VARCHAR(9)  NOT NULL,
    merch_id  VARCHAR(15) NOT NULL,
    amount    BIGINT      NOT NULL,
    appr_date CHAR(8)     NOT NULL,
    status    CHAR(1)     NOT NULL DEFAULT 'A',
    CONSTRAINT pk_approval PRIMARY KEY (appr_no)
);
'''


# ======================================================================
# 생성 로직
# ======================================================================
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--fraction", type=float, default=1.0,
                    help="목표 건수 대비 생성 비율 (테스트용)")
    args = ap.parse_args()
    frac = args.fraction

    def scaled(name, floor):
        return max(floor, int(round(TARGETS[name] * frac)))

    n_online = scaled("online", 11)
    n_batch  = scaled("batch", 11)
    n_common = scaled("common", 11)
    n_dbio   = scaled("dbio", 22)      # 11 main + extras
    n_dbsql  = scaled("dbio_sql", 11)
    n_fml    = scaled("fml", 11)
    n_sh     = scaled("sh", 11)
    n_mk     = scaled("mk", 11)
    n_config = scaled("config", 11)

    n_mods = len(MOD_CODES)
    n_dbio_extra = max(0, n_dbio - n_mods)   # main 은 모듈당 1개

    # 헤더: core(11) + io헤더(extra dbio 수) + cu헤더(common 수) + standalone
    n_hdr_target = scaled("headers", n_mods + 4)
    n_standalone = max(0, n_hdr_target - n_mods - n_dbio_extra - n_common)

    # ---- 디렉토리 초기화 -------------------------------------------------
    for sub in ["framework", "app", "fml", "scripts", "mk", "config", "db",
                "build", "bin"]:
        p = os.path.join(ROOT, sub)
        if os.path.isdir(p):
            shutil.rmtree(p)
    for f in ["Makefile", "inventory.json"]:
        p = os.path.join(ROOT, f)
        if os.path.isfile(p):
            os.remove(p)

    def wr(relpath, content):
        p = os.path.join(ROOT, relpath)
        os.makedirs(os.path.dirname(p), exist_ok=True)
        with open(p, "w", encoding="utf-8") as fh:
            fh.write(content)

    inventory = []
    counts = {}

    def bump(k, n=1):
        counts[k] = counts.get(k, 0) + n

    def loc_of(s):
        return s.count("\n") + (0 if s.endswith("\n") else 1)

    def sqlcount(s):
        return s.count("EXEC SQL")

    def add_inv(pid, module, archetype, tier, path, content):
        inventory.append({
            "id": pid,
            "module": module,
            "archetype": archetype,
            "tier": tier,
            "path": path,
            "loc": loc_of(content),
            "sqlCount": sqlcount(content),
        })

    # ---- 프레임워크 (공유) ----------------------------------------------
    wr("framework/txcore/include/txcore.h", TXCORE_H)
    wr("framework/txcore/dbio/txcore_dbio.h", TXCORE_DBIO_H)
    wr("framework/txcore/src/txcore.c", TXCORE_C)
    wr("app/shared/include/msg_layout.h", MSG_LAYOUT_H)
    wr("app/shared/include/acq_util.h", ACQ_UTIL_H)
    wr("app/shared/common/util_date.c", UTIL_DATE_C)
    wr("app/shared/common/util_amount.c", UTIL_AMOUNT_C)
    wr("app/shared/common/util_msg.c", UTIL_MSG_C)
    bump("framework", 3)
    bump("shared_hdr", 2)
    bump("shared_c", 3)

    # ---- 모듈 core 헤더 + dbio_main -------------------------------------
    first_online = {}   # mod -> flagship online id
    first_batch = {}    # mod -> flagship batch id

    for mod, kname, xform in MODULES:
        core = render(CORE_H_TPL, mod=mod, MOD=mod.upper(),
                      KMOD=kname, XFORM=xform)
        wr("app/include/%s_core.h" % mod, core)
        bump("headers")

        dm = render(DBIO_MAIN_TPL, mod=mod, MOD=mod.upper(),
                    KMOD=kname, XFORM=xform)
        pid = "%s_dbio_main" % mod
        path = "app/dbio/%s.pgc" % pid
        wr(path, dm)
        add_inv(pid, mod, "dbio", "medium", path, dm)
        bump("dbio")

    # ---- 온라인 서비스 ---------------------------------------------------
    seq = {m: 0 for m in MOD_CODES}
    for i in range(n_online):
        mod = MOD_CODES[i % n_mods]
        seq[mod] += 1
        pid = "%s_ol_%04d" % (mod, seq[mod])
        svc = pid.upper()
        tier = tier_for(i)
        kname = MOD_KNAME[mod]
        xform = MOD_XFORM[mod]
        if mod not in first_online:
            first_online[mod] = (pid, svc)
        body = {"easy": ONLINE_EASY, "medium": ONLINE_MEDIUM,
                "hard": ONLINE_HARD}[tier]
        content = render(ONLINE_HEAD + body, mod=mod, MOD=mod.upper(),
                         ID=pid, SVC=svc, TIER=tier, KMOD=kname, XFORM=xform,
                         NEXTSVC="%s_POST" % mod.upper())
        path = "app/online/%s.pgc" % pid
        wr(path, content)
        add_inv(pid, mod, "online", tier, path, content)
        bump("online")

    # ---- 배치 ------------------------------------------------------------
    bseq = {m: 0 for m in MOD_CODES}
    for i in range(n_batch):
        mod = MOD_CODES[i % n_mods]
        bseq[mod] += 1
        pid = "%s_bt_%04d" % (mod, bseq[mod])
        tier = tier_for(i)
        kname = MOD_KNAME[mod]
        xform = MOD_XFORM[mod]
        if mod not in first_batch:
            first_batch[mod] = pid
        content = render(BATCH_TPL, mod=mod, MOD=mod.upper(), ID=pid,
                         TIER=tier, KMOD=kname, XFORM=xform)
        path = "app/batch/%s.pgc" % pid
        wr(path, content)
        add_inv(pid, mod, "batch", tier, path, content)
        bump("batch")

    # ---- 추가 DBIO -------------------------------------------------------
    ioseq = {m: 0 for m in MOD_CODES}
    for i in range(n_dbio_extra):
        mod = MOD_CODES[i % n_mods]
        ioseq[mod] += 1
        pid = "%s_io_%04d" % (mod, ioseq[mod])
        tier = tier_for(i)
        kname = MOD_KNAME[mod]
        xform = MOD_XFORM[mod]
        hdr = render(DBIO_EXTRA_H, ID=pid, IDU=pid.upper(), KMOD=kname)
        wr("app/include/%s.h" % pid, hdr)
        bump("headers")
        content = render(DBIO_EXTRA_TPL, mod=mod, ID=pid, TIER=tier,
                         KMOD=kname, XFORM=xform)
        path = "app/dbio/%s.pgc" % pid
        wr(path, content)
        add_inv(pid, mod, "dbio", tier, path, content)
        bump("dbio")

    # ---- 공통 유틸 -------------------------------------------------------
    cseq = {m: 0 for m in MOD_CODES}
    for i in range(n_common):
        mod = MOD_CODES[i % n_mods]
        cseq[mod] += 1
        pid = "%s_cu_%04d" % (mod, cseq[mod])
        tier = tier_for(i)
        kname = MOD_KNAME[mod]
        xform = MOD_XFORM[mod]
        hdr = render(COMMON_H, ID=pid, IDU=pid.upper(), KMOD=kname)
        wr("app/include/%s.h" % pid, hdr)
        bump("headers")
        content = render(COMMON_TPL, mod=mod, ID=pid, TIER=tier,
                         KMOD=kname, XFORM=xform)
        path = "app/common/%s.c" % pid
        wr(path, content)
        add_inv(pid, mod, "common", tier, path, content)
        bump("common")

    # ---- 독립 헤더 -------------------------------------------------------
    hseq = {m: 0 for m in MOD_CODES}
    for i in range(n_standalone):
        mod = MOD_CODES[i % n_mods]
        hseq[mod] += 1
        pid = "%s_h_%04d" % (mod, hseq[mod])
        kname = MOD_KNAME[mod]
        tpl = HDR_ROTATION[i % len(HDR_ROTATION)]
        content = render(tpl, ID=pid, IDU=pid.upper(), KMOD=kname)
        wr("app/include/%s.h" % pid, content)
        bump("headers")

    # ---- 데모 서버 main (온라인 데모) -----------------------------------
    for mod in ONLINE_DEMOS:
        if mod not in first_online:
            continue
        _pid, svc = first_online[mod]
        content = render(DEMO_MAIN_TPL, mod=mod, SVC=svc, KMOD=MOD_KNAME[mod])
        wr("app/common/demo_%s_main.c" % mod, content)
        bump("demo_main")

    # ---- FML -------------------------------------------------------------
    for i in range(n_fml):
        mod = MOD_CODES[i % n_mods]
        pid = "%s_fml_%04d" % (mod, i // n_mods + 1)
        content = render(FML_TPL, ID=pid.upper(), KMOD=MOD_KNAME[mod],
                         BASE=(i % 9) + 1)
        wr("fml/%s.fml" % pid, content)
        bump("fml")

    # ---- 셸 러너 ---------------------------------------------------------
    for i in range(n_sh):
        mod = MOD_CODES[i % n_mods]
        pid = "%s_run_%04d" % (mod, i // n_mods + 1)
        tier = tier_for(i)
        runbin = "demo_%s_batch" % mod if mod in BATCH_DEMOS else \
                 first_batch.get(mod, "%s_bt_0001" % mod)
        content = render(SH_TPL, ID=pid, TIER=tier, KMOD=MOD_KNAME[mod],
                         RUNBIN=runbin)
        wr("scripts/%s.sh" % pid, content)
        os.chmod(os.path.join(ROOT, "scripts/%s.sh" % pid), 0o755)
        bump("sh")

    # ---- Makefile 조각 ---------------------------------------------------
    for i in range(n_mk):
        mod = MOD_CODES[i % n_mods]
        pid = "%s_frag_%04d" % (mod, i // n_mods + 1)
        tier = tier_for(i)
        content = render(MK_TPL, ID=pid, IDU=pid.upper(), mod=mod, TIER=tier,
                         KMOD=MOD_KNAME[mod], XFORM=MOD_XFORM[mod],
                         SUBDIR="online", REF="%s_ol_0001.pgc" % mod)
        wr("mk/%s.mk" % pid, content)
        bump("mk")

    # ---- 설정 (.ubb/.cfg/.dat) ------------------------------------------
    cfg_exts = ["ubb", "cfg", "dat"]
    for i in range(n_config):
        mod = MOD_CODES[i % n_mods]
        ext = cfg_exts[i % 3]
        pid = "%s_cfg_%04d" % (mod, i // n_mods + 1)
        if ext == "ubb":
            content = render(UBB_TPL, ID=pid, MOD=mod.upper(),
                             KMOD=MOD_KNAME[mod],
                             IPCKEY=str(70000 + i), GRPNO=str((i % 30) + 1))
        elif ext == "cfg":
            content = render(CFG_TPL, ID=pid, mod=mod,
                             XFORM=MOD_XFORM[mod], TIER=tier_for(i))
        else:
            content = render(DAT_TPL, ID=pid, KMOD=MOD_KNAME[mod])
        wr("config/%s.%s" % (pid, ext), content)
        bump("config")

    # ---- DBIO SQL 스키마 -------------------------------------------------
    wr("db/schema.sql", SCHEMA_SQL)
    for i in range(n_dbsql):
        mod = MOD_CODES[i % n_mods]
        pid = "%s_ddl_%04d" % (mod, i // n_mods + 1)
        content = render(SQL_TPL, ID=pid, mod=mod, KMOD=MOD_KNAME[mod],
                         TIER=tier_for(i))
        wr("db/%s.sql" % pid, content)
        bump("dbio_sql")

    # ---- Makefile --------------------------------------------------------
    wr("Makefile", build_makefile(first_online, first_batch))

    # ---- inventory.json --------------------------------------------------
    tier_hist = {}
    arche_hist = {}
    for r in inventory:
        tier_hist[r["tier"]] = tier_hist.get(r["tier"], 0) + 1
        arche_hist[r["archetype"]] = arche_hist.get(r["archetype"], 0) + 1

    inv_doc = {
        "corpus": "acquire-core-full",
        "domain": "card acquiring / settlement (카드 매입·정산)",
        "framework": "TxCore (ECPG + 고정길이 전문 + TP 관용구)",
        "generatedBy": "generate.py",
        "fraction": frac,
        "fileCounts": counts,
        "programCount": len(inventory),
        "byTier": tier_hist,
        "byArchetype": arche_hist,
        "modules": {m[0]: m[1] for m in MODULES},
        "programs": sorted(inventory, key=lambda r: r["path"]),
    }
    with open(os.path.join(ROOT, "inventory.json"), "w", encoding="utf-8") as fh:
        json.dump(inv_doc, fh, ensure_ascii=False, indent=2)

    # ---- 요약 출력 -------------------------------------------------------
    total_files = 0
    for _root, _dirs, files in os.walk(ROOT):
        if "/.git" in _root:
            continue
        for f in files:
            total_files += 1
    print("=== acquire-core-full 생성 완료 (fraction=%.3f) ===" % frac)
    for k in sorted(counts):
        print("  %-12s %5d" % (k, counts[k]))
    print("  programs(inv) %5d" % len(inventory))
    print("  tier         ", tier_hist)
    print("  총 파일수    ", total_files)


def build_makefile(first_online, first_batch):
    lines = []
    A = lines.append
    A("# ==========================================================================")
    A("# acquire-core-full / TxCore  Makefile  (생성기 산출물)")
    A("#")
    A("#   .pgc --ecpg--> .c --gcc--> build/*.o --ar--> libtxcore.a --ld--> bin/*")
    A("#")
    A("#   ~1000개 .c/.pgc 를 build/*.o 로 컴파일하는 것이 빌드 검증의 핵심.")
    A("#   와일드카드로 전체 소스를 수집하고, 대표 데모 바이너리 몇 개를 링크한다.")
    A("# ==========================================================================")
    A("CC        := gcc")
    A("ECPG      := ecpg")
    A("AR        := ar")
    A("PG_INCDIR := $(shell pg_config --includedir 2>/dev/null)")
    A("")
    A("INCLUDES  := -Iframework/txcore/include \\")
    A("             -Iframework/txcore/dbio \\")
    A("             -Iapp/include \\")
    A("             -Iapp/shared/include \\")
    A("             -I$(PG_INCDIR)")
    A("")
    A("# 고정길이 관용구 경고만 억제 (-Werror 없음)")
    A("CFLAGS    := -g -O2 -Wall -Wextra -Wno-unused-parameter \\")
    A("             -Wno-stringop-truncation -Wno-format-truncation \\")
    A("             -Wno-unused-variable -Wno-unused-but-set-variable $(INCLUDES)")
    A("ECPGFLAGS := -Iframework/txcore/include -Iframework/txcore/dbio \\")
    A("             -Iapp/include -Iapp/shared/include")
    A("LDLIBS    := -lecpg -lpq")
    A("")
    A("BUILD := build")
    A("BIN   := bin")
    A("")
    A("# ---- 소스 수집 (와일드카드) ----------------------------------------------")
    A("ONLINE_PGC := $(wildcard app/online/*.pgc)")
    A("BATCH_PGC  := $(wildcard app/batch/*.pgc)")
    A("DBIO_PGC   := $(wildcard app/dbio/*.pgc)")
    A("COMMON_C   := $(wildcard app/common/*.c)")
    A("SHARED_C   := $(wildcard app/shared/common/*.c)")
    A("")
    A("PGC_ALL := $(ONLINE_PGC) $(BATCH_PGC) $(DBIO_PGC)")
    A("GEN_C   := $(PGC_ALL:.pgc=.c)")
    A("")
    A("PGC_OBJ    := $(patsubst %,$(BUILD)/%.o,$(basename $(notdir $(PGC_ALL))))")
    A("COMMON_OBJ := $(patsubst %,$(BUILD)/%.o,$(basename $(notdir $(COMMON_C))))")
    A("SHARED_OBJ := $(patsubst %,$(BUILD)/%.o,$(basename $(notdir $(SHARED_C))))")
    A("ALL_OBJ    := $(PGC_OBJ) $(COMMON_OBJ) $(SHARED_OBJ)")
    A("")
    A("LIBTXCORE  := $(BUILD)/libtxcore.a")
    A("")
    # 데모 바이너리 목록
    demo_bins = []
    for mod in ONLINE_DEMOS:
        if mod in first_online:
            demo_bins.append("$(BIN)/demo_%s_server" % mod)
    for mod in BATCH_DEMOS:
        if mod in first_batch:
            demo_bins.append("$(BIN)/demo_%s_batch" % mod)
    A("DEMO_BINS := " + " ".join(demo_bins))
    A("")
    A(".SECONDARY:")
    A(".PHONY: all clean dirs gen objs demos")
    A("")
    A("all: dirs $(LIBTXCORE) objs demos")
    A("\t@echo \"==> 빌드 완료\"")
    A("\t@echo \"오브젝트: $(words $(ALL_OBJ)) 개\"")
    A("")
    A("dirs:")
    A("\t@mkdir -p $(BUILD) $(BIN)")
    A("")
    A("gen: $(GEN_C)")
    A("objs: $(ALL_OBJ)")
    A("demos: $(DEMO_BINS)")
    A("")
    A("# ---- ecpg: .pgc -> .c (디렉토리별) ---------------------------------------")
    A("app/online/%.c: app/online/%.pgc")
    A("\t$(ECPG) $(ECPGFLAGS) -o $@ $<")
    A("app/batch/%.c: app/batch/%.pgc")
    A("\t$(ECPG) $(ECPGFLAGS) -o $@ $<")
    A("app/dbio/%.c: app/dbio/%.pgc")
    A("\t$(ECPG) $(ECPGFLAGS) -o $@ $<")
    A("")
    A("# ---- 컴파일: build/<basename>.o (gen 선행 보장) --------------------------")
    A("$(BUILD)/%.o: app/online/%.c | gen")
    A("\t$(CC) $(CFLAGS) -c $< -o $@")
    A("$(BUILD)/%.o: app/batch/%.c | gen")
    A("\t$(CC) $(CFLAGS) -c $< -o $@")
    A("$(BUILD)/%.o: app/dbio/%.c | gen")
    A("\t$(CC) $(CFLAGS) -c $< -o $@")
    A("$(BUILD)/%.o: app/common/%.c")
    A("\t$(CC) $(CFLAGS) -c $< -o $@")
    A("$(BUILD)/%.o: app/shared/common/%.c")
    A("\t$(CC) $(CFLAGS) -c $< -o $@")
    A("")
    A("# ---- TxCore 정적 라이브러리 ----------------------------------------------")
    A("$(BUILD)/txcore.o: framework/txcore/src/txcore.c")
    A("\t$(CC) $(CFLAGS) -c $< -o $@")
    A("$(LIBTXCORE): $(BUILD)/txcore.o")
    A("\t$(AR) rcs $@ $^")
    A("")
    A("# ---- 대표 데모 바이너리 (링크 검증) --------------------------------------")
    for mod in ONLINE_DEMOS:
        if mod not in first_online:
            continue
        flag_id, _svc = first_online[mod]
        objs = "$(BUILD)/demo_%s_main.o $(BUILD)/%s.o $(BUILD)/%s_dbio_main.o $(SHARED_OBJ)" % (
            mod, flag_id, mod)
        A("$(BIN)/demo_%s_server: %s $(LIBTXCORE)" % (mod, objs))
        A("\t$(CC) $(CFLAGS) -o $@ %s -L$(BUILD) -ltxcore $(LDLIBS)" % objs)
        A("")
    for mod in BATCH_DEMOS:
        if mod not in first_batch:
            continue
        bid = first_batch[mod]
        objs = "$(BUILD)/%s.o $(BUILD)/%s_dbio_main.o $(SHARED_OBJ)" % (bid, mod)
        A("$(BIN)/demo_%s_batch: %s $(LIBTXCORE)" % (mod, objs))
        A("\t$(CC) $(CFLAGS) -o $@ %s -L$(BUILD) -ltxcore $(LDLIBS)" % objs)
        A("")
    A("# ==========================================================================")
    A("clean:")
    A("\trm -rf $(BUILD) $(BIN)")
    A("\trm -f $(GEN_C)")
    A("")
    return "\n".join(lines) + "\n"


if __name__ == "__main__":
    main()
