yazrkisvs3vvvsv33svrhetrjsvyvsvvyjsvsvs
tavsuysvssvvjvvyjyjkvsvqd
qfogsvvsvsegjdgdfgdskhgdksvqsvshdqsd
<?php
// api/tiktok-data.php — TikTok Business API v1.3
header('Content-Type: application/json');
require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../db.php';
require_once __DIR__ . '/../auth.php';
require_once __DIR__ . '/../credentials.php';
require_once __DIR__ . '/logger.php';
dm_require_login();
dm_check_module_access('tiktok_ads');

$client_id = (int)($_GET['client_id'] ?? 0);
$action    = $_GET['action']  ?? 'overview';
$period    = $_GET['period']  ?? '30d';
dm_require_client_access($client_id);

// ── Fechas ────────────────────────────────────────────────────────────────────
if (!empty($_GET['date_from']) && !empty($_GET['date_to']) &&
    preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['date_from']) &&
    preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['date_to'])) {
    $date_from = $_GET['date_from'];
    $date_to   = $_GET['date_to'];
    $cache_sfx = "{$date_from}_{$date_to}";
} else {
    $days_map  = ['7d'=>7,'14d'=>14,'30d'=>30,'60d'=>60,'90d'=>90];
    $days      = $days_map[$period] ?? 30;
    $date_from = date('Y-m-d', strtotime("-{$days} days"));
    $date_to   = date('Y-m-d');
    $cache_sfx = $period;
}

// ── Cache ─────────────────────────────────────────────────────────────────────
$cache_key = "tiktok_{$action}_{$client_id}_{$cache_sfx}";
$cached    = dm_cache_get($cache_key);
if ($cached) { echo json_encode(['ok'=>true,'data'=>$cached,'cached'=>true]); exit; }

// ── Credenciales del cliente ──────────────────────────────────────────────────
$creds        = dm_get_credentials($client_id, 'tiktok');
$access_token = $creds['access_token']  ?? '';
$adv_id       = $creds['advertiser_id'] ?? '';

if (!$access_token || !$adv_id) {
    echo json_encode(['ok'=>false,'error'=>'tiktok_not_connected',
        'message'=>'TikTok no conectado. Ve a Configuración de APIs para conectar la cuenta.']);
    exit;
}

$base    = 'https://business-api.tiktok.com';
$headers = ["Access-Token: {$access_token}", 'Content-Type: application/json'];

// ── Helpers HTTP ──────────────────────────────────────────────────────────────
function tt_get(string $url, array $headers): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_TIMEOUT        => 20,
    ]);
    $raw   = curl_exec($ch);
    $errno = curl_errno($ch);
    curl_close($ch);
    if ($errno || !$raw) return ['code'=>-1,'message'=>'Error de red'];
    return json_decode($raw, true) ?? ['code'=>-1,'message'=>'Respuesta inválida'];
}

function tt_post(string $url, array $body, array $headers): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($body),
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_TIMEOUT        => 25,
    ]);
    $raw   = curl_exec($ch);
    $errno = curl_errno($ch);
    curl_close($ch);
    if ($errno || !$raw) return ['code'=>-1,'message'=>'Error de red'];
    return json_decode($raw, true) ?? ['code'=>-1,'message'=>'Respuesta inválida'];
}

// ── Formateadores ─────────────────────────────────────────────────────────────
function tt_fmt_money(float $v): string {
    if ($v >= 1000000) return '$' . number_format($v/1000000, 1) . 'M';
    if ($v >= 1000)    return '$' . number_format($v/1000, 1) . 'K';
    return '$' . number_format($v, 0, ',', '.');
}
function tt_fmt_num(int $v): string {
    if ($v >= 1000000) return number_format($v/1000000, 1) . 'M';
    if ($v >= 1000)    return number_format($v/1000, 1) . 'K';
    return number_format($v, 0, ',', '.');
}
function tt_fmt_pct(float $v): string { return number_format($v, 2) . '%'; }
function tt_status_es(string $s): string {
    return ['ENABLE'=>'Activa','DISABLE'=>'Pausada','DELETE'=>'Eliminada'][$s] ?? strtolower($s);
}

// ── ACCIÓN: overview — métricas totales del período ──────────────────────────
if ($action === 'overview') {
    $res = tt_post("{$base}/open_api/v1.3/report/integrated/get/", [
        'advertiser_id' => $adv_id,
        'report_type'   => 'AUCTION',
        'dimensions'    => ['campaign_id'],
        'metrics'       => ['spend','impressions','clicks','ctr','cpm','conversion','cost_per_conversion','reach'],
        'start_date'    => $date_from,
        'end_date'      => $date_to,
        'page_size'     => 50,
        'page'          => 1,
    ], $headers);

    if (($res['code'] ?? -1) !== 0) {
        $code = $res['code'] ?? -1;
        $msg  = $res['message'] ?? 'Error TikTok API';
        if ($code == 40001 || $code == 40002) $msg = 'token_expired: ' . $msg;
        echo json_encode(['ok'=>false,'error'=> $msg]); exit;
    }

    $rows    = $res['data']['list'] ?? [];
    $spend   = $impr = $clicks = $conv = $reach = $cpm_sum = 0;
    $n       = 0;
    foreach ($rows as $r) {
        $m       = $r['metrics'] ?? [];
        $spend  += (float)($m['spend']                ?? 0);
        $impr   += (int)  ($m['impressions']          ?? 0);
        $clicks += (int)  ($m['clicks']               ?? 0);
        $conv   += (int)  ($m['conversion']           ?? 0);
        $reach  += (int)  ($m['reach']                ?? 0);
        $cpm_sum += (float)($m['cpm']                 ?? 0);
        $n++;
    }
    $n   = max($n, 1);
    $ctr = $impr > 0 ? round($clicks / $impr * 100, 2) : 0;
    $cpm = round($cpm_sum / $n, 2);
    $cpa = $conv > 0 ? round($spend / $conv, 2) : 0;

    $data = [
        'spend'       => $spend,       'spend_fmt'   => tt_fmt_money($spend),
        'impressions' => $impr,        'impr_fmt'    => tt_fmt_num($impr),
        'clicks'      => $clicks,      'clicks_fmt'  => tt_fmt_num($clicks),
        'conversions' => $conv,        'conv_fmt'    => tt_fmt_num($conv),
        'reach'       => $reach,       'reach_fmt'   => tt_fmt_num($reach),
        'ctr'         => $ctr,         'ctr_fmt'     => tt_fmt_pct($ctr),
        'cpm'         => $cpm,         'cpm_fmt'     => tt_fmt_money($cpm),
        'cpa'         => $cpa,         'cpa_fmt'     => $conv > 0 ? tt_fmt_money($cpa) : '—',
    ];
    dm_cache_set($cache_key, $data, 30);
    echo json_encode(['ok'=>true,'data'=>$data]);
    exit;
}

// ── ACCIÓN: campaigns — lista con métricas ───────────────────────────────────
if ($action === 'campaigns') {
    $camp_res = tt_get(
        "{$base}/open_api/v1.3/campaign/get/?" . http_build_query([
            'advertiser_id' => $adv_id,
            'fields'        => json_encode(['campaign_id','campaign_name','status','objective_type','budget','budget_mode']),
            'page_size'     => 50,
            'page'          => 1,
        ]),
        $headers
    );
    if (($camp_res['code'] ?? -1) !== 0) {
        echo json_encode(['ok'=>false,'error'=> $camp_res['message'] ?? 'Error al traer campañas']); exit;
    }
    $camps    = $camp_res['data']['list'] ?? [];
    $camp_ids = array_column($camps, 'campaign_id');

    // Métricas del período agrupadas por campaign_id
    $metrics_by_camp = [];
    if (!empty($camp_ids)) {
        $rpt = tt_post("{$base}/open_api/v1.3/report/integrated/get/", [
            'advertiser_id' => $adv_id,
            'report_type'   => 'AUCTION',
            'dimensions'    => ['campaign_id'],
            'metrics'       => ['spend','impressions','clicks','ctr','cpm','conversion','cost_per_conversion'],
            'start_date'    => $date_from,
            'end_date'      => $date_to,
            'filtering'     => [['field_name'=>'campaign_id','filter_type'=>'IN','filter_value'=>json_encode($camp_ids)]],
            'page_size'     => 50,
            'page'          => 1,
        ], $headers);
        foreach ($rpt['data']['list'] ?? [] as $row) {
            $cid = $row['dimensions']['campaign_id'] ?? '';
            $metrics_by_camp[$cid] = $row['metrics'] ?? [];
        }
    }

    $list = [];
    foreach ($camps as $c) {
        $cid   = $c['campaign_id'];
        $m     = $metrics_by_camp[$cid] ?? [];
        $spend = (float)($m['spend'] ?? 0);
        $conv  = (int)  ($m['conversion'] ?? 0);
        $list[] = [
            'id'          => $cid,
            'name'        => $c['campaign_name'],
            'status'      => tt_status_es($c['status'] ?? ''),
            'status_raw'  => $c['status'] ?? '',
            'objective'   => $c['objective_type'] ?? '',
            'budget'      => (float)($c['budget'] ?? 0),
            'budget_mode' => $c['budget_mode'] ?? '',
            'budget_fmt'  => tt_fmt_money((float)($c['budget'] ?? 0)),
            'spend'       => $spend,
            'spend_fmt'   => tt_fmt_money($spend),
            'impressions' => (int)($m['impressions'] ?? 0),
            'clicks'      => (int)($m['clicks'] ?? 0),
            'ctr'         => round((float)($m['ctr'] ?? 0), 2),
            'ctr_fmt'     => tt_fmt_pct((float)($m['ctr'] ?? 0)),
            'cpm'         => round((float)($m['cpm'] ?? 0), 2),
            'cpm_fmt'     => tt_fmt_money((float)($m['cpm'] ?? 0)),
            'conversions' => $conv,
            'cpa_fmt'     => $conv > 0 ? tt_fmt_money($spend / $conv) : '—',
        ];
    }
    dm_cache_set($cache_key, $list, 30);
    echo json_encode(['ok'=>true,'data'=>$list]);
    exit;
}

// ── ACCIÓN: adgroups — lista de Ad Groups con métricas ───────────────────────
if ($action === 'adgroups') {
    $campaign_id = preg_replace('/[^0-9]/', '', $_GET['campaign_id'] ?? '');
    $params = [
        'advertiser_id' => $adv_id,
        'fields'        => json_encode(['adgroup_id','adgroup_name','campaign_id','status',
                                        'budget','budget_mode','optimization_goal','bid_type']),
        'page_size'     => 50,
        'page'          => 1,
    ];
    if ($campaign_id) $params['campaign_ids'] = json_encode([$campaign_id]);

    $res = tt_get("{$base}/open_api/v1.3/adgroup/get/?" . http_build_query($params), $headers);
    if (($res['code'] ?? -1) !== 0) {
        echo json_encode(['ok'=>false,'error'=> $res['message'] ?? 'Error al traer Ad Groups']); exit;
    }
    $groups = $res['data']['list'] ?? [];
    $ag_ids = array_column($groups, 'adgroup_id');

    // Métricas por adgroup_id
    $metrics_by_ag = [];
    if (!empty($ag_ids)) {
        $rpt = tt_post("{$base}/open_api/v1.3/report/integrated/get/", [
            'advertiser_id' => $adv_id,
            'report_type'   => 'AUCTION',
            'dimensions'    => ['adgroup_id'],
            'metrics'       => ['spend','impressions','clicks','ctr','cpm','conversion','cost_per_conversion'],
            'start_date'    => $date_from,
            'end_date'      => $date_to,
            'page_size'     => 50,
            'page'          => 1,
        ], $headers);
        foreach ($rpt['data']['list'] ?? [] as $row) {
            $agid = $row['dimensions']['adgroup_id'] ?? '';
            $metrics_by_ag[$agid] = $row['metrics'] ?? [];
        }
    }

    $list = [];
    foreach ($groups as $g) {
        $agid  = $g['adgroup_id'];
        $m     = $metrics_by_ag[$agid] ?? [];
        $spend = (float)($m['spend'] ?? 0);
        $conv  = (int)  ($m['conversion'] ?? 0);
        $list[] = [
            'id'          => $agid,
            'name'        => $g['adgroup_name'],
            'campaign_id' => $g['campaign_id'],
            'status'      => tt_status_es($g['status'] ?? ''),
            'status_raw'  => $g['status'] ?? '',
            'goal'        => $g['optimization_goal'] ?? '',
            'bid_type'    => $g['bid_type'] ?? '',
            'budget'      => (float)($g['budget'] ?? 0),
            'budget_fmt'  => tt_fmt_money((float)($g['budget'] ?? 0)),
            'spend'       => $spend,
            'spend_fmt'   => tt_fmt_money($spend),
            'impressions' => (int)($m['impressions'] ?? 0),
            'clicks'      => (int)($m['clicks'] ?? 0),
            'ctr'         => round((float)($m['ctr'] ?? 0), 2),
            'ctr_fmt'     => tt_fmt_pct((float)($m['ctr'] ?? 0)),
            'cpm'         => round((float)($m['cpm'] ?? 0), 2),
            'cpm_fmt'     => tt_fmt_money((float)($m['cpm'] ?? 0)),
            'conversions' => $conv,
            'cpa_fmt'     => $conv > 0 ? tt_fmt_money($spend / $conv) : '—',
        ];
    }
    dm_cache_set($cache_key, $list, 30);
    echo json_encode(['ok'=>true,'data'=>$list]);
    exit;
}

// ── ACCIÓN: compare — métricas del período anterior ─────────────────────────
if ($action === 'compare') {
    // Calcular duración del período actual y proyectar el período anterior
    $dt_from = new DateTime($date_from);
    $dt_to   = new DateTime($date_to);
    $duration = $dt_from->diff($dt_to)->days; // número de días

    $prev_to   = (clone $dt_from)->modify('-1 day');
    $prev_from = (clone $prev_to)->modify("-{$duration} days");

    $prev_from_str = $prev_from->format('Y-m-d');
    $prev_to_str   = $prev_to->format('Y-m-d');

    $res = tt_post("{$base}/open_api/v1.3/report/integrated/get/", [
        'advertiser_id' => $adv_id,
        'report_type'   => 'AUCTION',
        'dimensions'    => ['campaign_id'],
        'metrics'       => ['spend','impressions','clicks','ctr','cpm','conversion','cost_per_conversion','reach'],
        'start_date'    => $prev_from_str,
        'end_date'      => $prev_to_str,
        'page_size'     => 50,
        'page'          => 1,
    ], $headers);

    if (($res['code'] ?? -1) !== 0) {
        // No disponible — devolver prev null sin error fatal
        echo json_encode(['ok'=>true,'prev'=>null]);
        exit;
    }

    $rows    = $res['data']['list'] ?? [];
    $spend   = $impr = $clicks = $conv = $reach = $cpm_sum = 0;
    $n       = 0;
    foreach ($rows as $r) {
        $m       = $r['metrics'] ?? [];
        $spend  += (float)($m['spend']       ?? 0);
        $impr   += (int)  ($m['impressions'] ?? 0);
        $clicks += (int)  ($m['clicks']      ?? 0);
        $conv   += (int)  ($m['conversion']  ?? 0);
        $reach  += (int)  ($m['reach']       ?? 0);
        $cpm_sum += (float)($m['cpm']        ?? 0);
        $n++;
    }
    $n   = max($n, 1);
    $ctr = $impr > 0 ? round($clicks / $impr * 100, 2) : 0;
    $cpm = round($cpm_sum / $n, 2);
    $cpa = $conv > 0 ? round($spend / $conv, 2) : 0;

    $prev = [
        'spend'       => $spend,
        'impressions' => $impr,
        'clicks'      => $clicks,
        'ctr'         => $ctr,
        'reach'       => $reach,
        'conversions' => $conv,
        'cpa'         => $cpa,
        'cpm'         => $cpm,
    ];

    echo json_encode(['ok'=>true,'prev'=>$prev,'period_prev'=>"{$prev_from_str} / {$prev_to_str}"]);
    exit;
}

// ── ACCIONES DE ESCRITURA (POST) ──────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    dm_csrf_verify();
    $body    = json_decode(file_get_contents('php://input'), true) ?? [];
    $paction = $body['action'] ?? '';

    if ($paction === 'pause_campaign' || $paction === 'activate_campaign') {
        $camp_id = preg_replace('/[^0-9]/', '', $body['campaign_id'] ?? '');
        if (!$camp_id) { echo json_encode(['ok'=>false,'error'=>'campaign_id requerido']); exit; }
        $op_status = $paction === 'pause_campaign' ? 'DISABLE' : 'ENABLE';
        $res = tt_post("{$base}/open_api/v1.3/campaign/status/update/",
            ['advertiser_id'=>$adv_id,'campaign_ids'=>json_encode([$camp_id]),'operation_status'=>$op_status], $headers);
        if (($res['code']??-1)!==0) {
            DmLog::error('tiktok', 'campaign status update failed', ['action'=>$paction,'camp_id'=>$camp_id,'response'=>$res]);
            echo json_encode(['ok'=>false,'error'=>$res['message']??'Error TikTok API']); exit;
        }
        echo json_encode(['ok'=>true,'message'=>($paction==='pause_campaign'?'Campaña TikTok pausada':'Campaña TikTok activada')]);
        exit;
    }

    if ($paction === 'change_tiktok_budget') {
        $camp_id = preg_replace('/[^0-9]/', '', $body['campaign_id'] ?? '');
        $budget  = (float)($body['budget'] ?? 0);
        // FASE 2 FIX: validar rango razonable (min $1, max $50K diario) — evita
        // valores negativos, cero o irracionalmente altos por error de usuario o API mal usada.
        if (!$camp_id) { echo json_encode(['ok'=>false,'error'=>'campaign_id requerido']); exit; }
        if ($budget < 1) { echo json_encode(['ok'=>false,'error'=>'Presupuesto mínimo: $1/día']); exit; }
        if ($budget > 50000) { echo json_encode(['ok'=>false,'error'=>'Presupuesto máximo: $50.000/día. Para más, contacta al equipo.']); exit; }
        $res = tt_post("{$base}/open_api/v1.3/campaign/update/",
            ['advertiser_id'=>$adv_id,'campaign_id'=>$camp_id,'budget'=>$budget,'budget_mode'=>'BUDGET_MODE_DAY'], $headers);
        if (($res['code']??-1)!==0) { echo json_encode(['ok'=>false,'error'=>$res['message']??'Error TikTok API']); exit; }
        echo json_encode(['ok'=>true,'message'=>'Presupuesto TikTok actualizado a $'.number_format($budget,0)]);
        exit;
    }

    if ($paction === 'pause_adgroup' || $paction === 'activate_adgroup') {
        $ag_id = preg_replace('/[^0-9]/', '', $body['adgroup_id'] ?? '');
        if (!$ag_id) { echo json_encode(['ok'=>false,'error'=>'adgroup_id requerido']); exit; }
        $op_status = $paction === 'pause_adgroup' ? 'DISABLE' : 'ENABLE';
        $res = tt_post("{$base}/open_api/v1.3/adgroup/status/update/",
            ['advertiser_id'=>$adv_id,'adgroup_ids'=>json_encode([$ag_id]),'operation_status'=>$op_status], $headers);
        if (($res['code']??-1)!==0) { echo json_encode(['ok'=>false,'error'=>$res['message']??'Error TikTok API']); exit; }
        echo json_encode(['ok'=>true,'message'=>($paction==='pause_adgroup'?'Ad Group TikTok pausado':'Ad Group TikTok activado')]);
        exit;
    }

    echo json_encode(['ok'=>false,'error'=>'Acción POST no reconocida: '.htmlspecialchars($paction)]); exit;
}

echo json_encode(['ok'=>false,'error'=>'Acción no reconocida: ' . htmlspecialchars($action)]);
