// SPDX-License-Identifier: GPL-2.0-only
/*
 *  ppm/src/core/version_req.c
 *
 *  Copyright (C) 2026 Project Tick
 */

#include "version_req.h"

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

#include "../util/log.h"

/* Operators we recognise. */
typedef enum {
    OP_EQ,   /* = or bare exact */
    OP_GE,   /* >= */
    OP_GT,   /* >  */
    OP_LE,   /* <= */
    OP_LT,   /* <  */
    OP_CARET,/* ^  */
    OP_TILDE,/* ~  */
} op_t;

static void trim(const char **s, size_t *n) {
    while (*n && isspace((unsigned char)(*s)[0])) { (*s)++; (*n)--; }
    while (*n && isspace((unsigned char)(*s)[*n - 1])) { (*n)--; }
}

/*
 * Parse one constraint slice (a..a+n) into op + version. The version
 * triple defaults missing minor/patch to 0 so "1" -> 1.0.0.
 */
static bool parse_one(const char *a, size_t n, op_t *out_op,
                      ppm_version_t *out_v) {
    trim(&a, &n);
    if (n == 0) return false;

    op_t op = OP_EQ;
    size_t consume = 0;
    if (n >= 2 && a[0] == '>' && a[1] == '=') { op = OP_GE;    consume = 2; }
    else if (n >= 2 && a[0] == '<' && a[1] == '=') { op = OP_LE;    consume = 2; }
    else if (a[0] == '>')                          { op = OP_GT;    consume = 1; }
    else if (a[0] == '<')                          { op = OP_LT;    consume = 1; }
    else if (a[0] == '=')                          { op = OP_EQ;    consume = 1; }
    else if (a[0] == '^')                          { op = OP_CARET; consume = 1; }
    else if (a[0] == '~')                          { op = OP_TILDE; consume = 1; }

    a += consume; n -= consume;
    trim(&a, &n);
    if (n == 0) return false;

    /* Copy into a NUL-terminated scratch buffer for sscanf-like parsing. */
    char buf[64];
    if (n >= sizeof(buf)) return false;
    memcpy(buf, a, n);
    buf[n] = '\0';

    /* Strip trailing prerelease/build metadata for now — we match on
     * major/minor/patch only when comparing requirements. */
    char *dash  = strchr(buf, '-');
    char *plus  = strchr(buf, '+');
    if (dash) *dash = '\0';
    if (plus) *plus = '\0';

    unsigned maj = 0, min = 0, pat = 0;
    int filled = sscanf(buf, "%u.%u.%u", &maj, &min, &pat);
    if (filled < 1) return false;

    *out_op = op;
    out_v->major = maj;
    out_v->minor = min;
    out_v->patch = pat;
    out_v->pre = NULL;
    out_v->build = NULL;
    return true;
}

/* Compare only major/minor/patch (ignoring prerelease for requirement matching). */
static int cmp_mmp(const ppm_version_t *a, const ppm_version_t *b) {
    if (a->major != b->major) return (a->major < b->major) ? -1 : 1;
    if (a->minor != b->minor) return (a->minor < b->minor) ? -1 : 1;
    if (a->patch != b->patch) return (a->patch < b->patch) ? -1 : 1;
    return 0;
}

static bool match_one(op_t op, const ppm_version_t *req_v,
                      const ppm_version_t *v) {
    int c = cmp_mmp(v, req_v);
    switch (op) {
    case OP_EQ:    return c == 0;
    case OP_GE:    return c >= 0;
    case OP_GT:    return c >  0;
    case OP_LE:    return c <= 0;
    case OP_LT:    return c <  0;
    case OP_CARET: {
        /* >= req_v, < next-major (or < 0.(minor+1).0 if major == 0). */
        if (c < 0) return false;
        if (req_v->major != 0) return v->major == req_v->major;
        if (req_v->minor != 0) {
            return v->major == 0 && v->minor == req_v->minor;
        }
        /* ^0.0.x -> exactly 0.0.x */
        return v->major == 0 && v->minor == 0 && v->patch == req_v->patch;
    }
    case OP_TILDE:
        if (c < 0) return false;
        return v->major == req_v->major && v->minor == req_v->minor;
    }
    return false;
}

bool ppm_version_req_match(const char *req, const ppm_version_t *v) {
    if (!req || !v) return false;

    /* Empty / "*" requirement: match anything. */
    const char *r = req;
    while (*r && isspace((unsigned char)*r)) r++;
    if (*r == '\0' || (r[0] == '*' && (r[1] == '\0' || isspace((unsigned char)r[1]))))
        return true;

    /* Split on commas; every clause must match. */
    const char *p = req;
    while (*p) {
        const char *comma = strchr(p, ',');
        size_t n = comma ? (size_t)(comma - p) : strlen(p);
        op_t op;
        ppm_version_t req_v;
        if (!parse_one(p, n, &op, &req_v)) {
            ppm_warn("version_req: cannot parse constraint '%.*s'", (int)n, p);
            return false;
        }
        if (!match_one(op, &req_v, v)) return false;
        if (!comma) break;
        p = comma + 1;
    }
    return true;
}
