// =====================================================================
// artifact_file.sx — platform-agnostic, on-disk artifact validation
// (subplan 05, "Common Validation" subset).
//
// Given an artifact on disk plus the metadata a publish manifest declares
// for it (size, sha256, platform, content type), run the checks every
// artifact must pass regardless of platform and return a typed outcome
// that names WHICH check failed:
//
//   1. file exists at `path`;
//   2. on-disk size equals the declared `expected_size`;
//   3. SHA-256 of the file (recomputed via `std.hash`) equals the declared
//      `expected_sha256` (lowercase-hex);
//   4. `content_type` is on the accepted allow-list;
//   5. the file extension matches the platform's policy
//      (.apk -> android_apk, .ipa -> ios, ...).
//
// Checks run in that order, so the FIRST failing check sets the reason —
// e.g. a wrong size surfaces as `size_mismatch` even though the digest
// would also differ. The status reuses the P2.1 `ValidationStatus`: all
// checks pass -> `.valid`, any check fails -> `.invalid` (the manifest's
// "passed" / "failed"). Deep IPA/APK zip inspection is deferred.
// =====================================================================

#import "modules/std.sx";
#import "modules/std/fs.sx";
#import "modules/std/hash.sx";
#import "../domain/platform.sx";
#import "../domain/artifact.sx";

// Which common check failed (or `ok` when none did). One distinct tag per
// failure class so the caller can report a precise reason, not a bare bool.
ValidationReason :: enum u8 {
    ok;                   // all checks passed
    missing_file;         // no file at `path`
    size_mismatch;        // on-disk size != expected_size
    digest_mismatch;      // recomputed sha256 != expected_sha256
    content_type_denied;  // content_type not on the allow-list
    extension_mismatch;   // extension does not match the platform policy
}

// Result of the common validation pass: a P2.1 `ValidationStatus`
// (`.valid` = passed, `.invalid` = failed; `.pending` is never returned —
// it is the pre-validation state) paired with the specific reason.
ValidationOutcome :: struct {
    status: ValidationStatus;
    reason: ValidationReason;
}

fail :: (r: ValidationReason) -> ValidationOutcome {
    return ValidationOutcome.{ status = .invalid, reason = r };
}

// ── allow-lists / policy maps ─────────────────────────────────────────

// Accepted artifact content types. A declared type outside this set fails
// with `content_type_denied`.
is_allowed_content_type :: (ct: string) -> bool {
    if ct == "application/vnd.android.package-archive" { return true; } // .apk
    if ct == "application/octet-stream"                { return true; } // .ipa / generic binary
    if ct == "application/zip"                         { return true; } // archives
    if ct == "application/x-apple-diskimage"           { return true; } // .dmg
    if ct == "application/x-msdownload"                { return true; } // .exe / .msi
    return false;
}

// The file extension a given platform's artifact must carry (lowercase,
// no dot). Drives the extension/platform policy check.
expected_ext :: (p: Platform) -> string {
    if p == .ios         { return "ipa"; }
    if p == .android_apk { return "apk"; }
    if p == .macos       { return "dmg"; }
    if p == .linux       { return "appimage"; }
    if p == .windows     { return "exe"; }
    return "";
}

// The extension of `path` (the bytes after the last '.', as a zero-copy
// view). "" when the basename has no dot.
ext_of :: (path: string) -> string {
    i := path.len;
    while i > 0 {
        i -= 1;
        c := path[i];
        if c == 47 { return ""; }           // 47='/' : hit a separator, no extension
        if c == 46 {                         // 46='.'
            start := i + 1;
            if start >= path.len { return ""; }
            return string.{ ptr = @path[start], len = path.len - start };
        }
    }
    return "";
}

// Case-insensitive ASCII equality (extensions compare case-insensitively,
// so ".APK" matches the "apk" policy).
ieq_ascii :: (a: string, b: string) -> bool {
    if a.len != b.len { return false; }
    i := 0;
    while i < a.len {
        ca := a[i];
        cb := b[i];
        if ca >= 65 and ca <= 90 { ca += 32; }   // 'A'..'Z' -> lower
        if cb >= 65 and cb <= 90 { cb += 32; }
        if ca != cb { return false; }
        i += 1;
    }
    return true;
}

ext_matches_platform :: (path: string, p: Platform) -> bool {
    return ieq_ascii(ext_of(path), expected_ext(p));
}

// ── public API ───────────────────────────────────────────────────────

// Run the common validation pass on the file at `path`. Reads the file once
// to derive both its on-disk size and its SHA-256 (via `std.hash`). Returns
// `.valid`/`.ok` when every check passes, else `.invalid` with the reason of
// the first failing check (see the check order at the top of the file).
validate_artifact_file :: (
    path: string,
    expected_size: i64,
    expected_sha256: string,
    platform: Platform,
    content_type: string,
) -> ValidationOutcome {
    if !exists(path) { return fail(.missing_file); }

    src := read_file(path);
    if src == null { return fail(.missing_file); }
    bytes := src!;

    if bytes.len != expected_size { return fail(.size_mismatch); }

    digest := hash.sha256_hex(bytes);
    view := string.{ ptr = @digest[0], len = 64 };
    if view != expected_sha256 { return fail(.digest_mismatch); }

    if !is_allowed_content_type(content_type) { return fail(.content_type_denied); }

    if !ext_matches_platform(path, platform) { return fail(.extension_mismatch); }

    return ValidationOutcome.{ status = .valid, reason = .ok };
}
