using System.ComponentModel.DataAnnotations;
namespace JobsMedical.Web.Models;
///
/// Single-row (Id=1) platform settings the admin controls at runtime — chiefly the ingestion
/// automation policy and the optional AI audit layer. Kept in the DB (not appsettings) so it's
/// editable from the admin panel without a redeploy.
///
public class AppSetting
{
public int Id { get; set; } = 1;
// --- Ingestion automation ---
public IngestionMode Mode { get; set; } = IngestionMode.Manual;
/// In Automatic mode WITHOUT AI, listings at/above this confidence auto-publish.
public int AutoPublishMinConfidence { get; set; } = 85;
// --- AI audit layer (optional) ---
public bool AiEnabled { get; set; } = false;
/// OpenAI-compatible chat-completions endpoint (self-hosted or Iranian provider).
[MaxLength(500)] public string? AiEndpoint { get; set; }
[MaxLength(200)] public string? AiApiKey { get; set; }
[MaxLength(120)] public string? AiModel { get; set; } = "gpt-4o-mini";
/// The prompt + "framework" the AI follows to approve / reject / structure a listing.
[MaxLength(4000)]
public string AiSystemPrompt { get; set; } = DefaultPrompt;
/// If AI approves AND Mode is Automatic, publish without human review.
public bool AiAutoApprove { get; set; } = false;
/// Route AI calls through the ingestion proxy (IngestProxyUrl) — needed when the AI
/// endpoint (e.g. api.openai.com) is blocked in Iran.
public bool AiUseProxy { get; set; } = false;
// --- Channel scraping sources (configured here, NOT in env) ---
/// Run the ingestion worker on a timer.
public bool AutoIngestEnabled { get; set; } = false;
public int IngestIntervalMinutes { get; set; } = 30;
public bool TelegramEnabled { get; set; } = false;
/// Public Telegram channel usernames, one per line or comma-separated.
[MaxLength(2000)] public string? TelegramChannels { get; set; }
public bool BaleEnabled { get; set; } = false;
[MaxLength(200)] public string? BaleBotToken { get; set; }
/// Demo mode — keep the sample Tehran board seeded/visible (for showcasing).
public bool DemoMode { get; set; } = false;
public bool WebsitesEnabled { get; set; } = false;
/// Generic web pages to scrape, one URL per line.
[MaxLength(4000)] public string? WebsiteUrls { get; set; }
/// Local proxy an Xray/V2Ray client sidecar exposes, e.g. socks5://xray:10808
/// (also accepts socks4:// or http://). The app cannot read vmess/vless/trojan directly;
/// the sidecar converts that config into this local proxy. Per-source toggles below decide
/// which channels actually route through it.
[MaxLength(200)] public string? IngestProxyUrl { get; set; }
/// Legacy global flag — kept for compatibility; per-source flags below now control routing.
public bool IngestProxyEnabled { get; set; } = false;
// Per-source: route this source's fetches through IngestProxyUrl (only when a URL is set).
public bool TelegramUseProxy { get; set; } = false;
public bool BaleUseProxy { get; set; } = false;
public bool DivarUseProxy { get; set; } = false;
public bool MedjobsUseProxy { get; set; } = false;
public bool WebsitesUseProxy { get; set; } = false;
public bool DivarEnabled { get; set; } = false;
[MaxLength(60)] public string? DivarCity { get; set; } = "tehran";
/// Divar search terms, one per line or comma-separated.
[MaxLength(2000)] public string? DivarQueries { get; set; }
/// Scrape medjobs.ir job ads (WordPress classifieds — crawled via its sitemaps).
public bool MedjobsEnabled { get; set; } = false;
/// Max ads to fetch per ingestion run (be polite; dedupe skips already-seen).
public int MedjobsMaxAds { get; set; } = 40;
// --- SMS OTP (Kavenegar). When off, the code is shown on screen (dev only). ---
public bool SmsEnabled { get; set; } = false;
[MaxLength(200)] public string? SmsApiKey { get; set; }
/// Kavenegar verify/lookup template name (preferred OTP method in Iran).
[MaxLength(100)] public string? SmsTemplate { get; set; }
/// Sender line for plain SMS fallback when no template is set.
[MaxLength(30)] public string? SmsSender { get; set; }
/// Neshan web map.js API key — enables the click-to-pick map on the facility form
/// (Google Maps is blocked in Iran). Empty → only the "my location" button is shown.
[MaxLength(200)] public string? NeshanMapKey { get; set; }
// --- Notification channels (master on/off, controlled from the admin panel) ---
/// Live in-app / web notifications (SSE bell + toast + local OS popup). Works in Iran
/// because it streams over our own origin — no external push service. On by default.
public bool WebNotificationsEnabled { get; set; } = true;
// --- Web Push (PWA notifications). VAPID keypair; generate once with the web-push tooling. ---
public bool PushEnabled { get; set; } = false;
[MaxLength(200)] public string? VapidPublicKey { get; set; }
[MaxLength(200)] public string? VapidPrivateKey { get; set; }
[MaxLength(120)] public string? VapidSubject { get; set; } = "mailto:admin@hamkadr.ir";
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
/// Split a textarea (newline/comma separated) into trimmed non-empty items.
public static List SplitList(string? s) => string.IsNullOrWhiteSpace(s)
? new()
: s.Split(new[] { '\n', '\r', ',', '،' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.ToList();
public const string DefaultPrompt = """
تو دستیار بررسی آگهیهای کاری حوزه درمان برای پلتفرم «همکادر» هستی.
هر آگهی خام را بخوان و تصمیم بگیر:
- approve: آگهی واقعی و مرتبط با شیفت/استخدام کادر درمان است و اطلاعات کافی دارد.
- reject: تبلیغ، اسپم، نامرتبط، یا فاقد اطلاعات حداقلی است.
- review: مرتبط است اما ناقص/مبهم و نیاز به بررسی انسانی دارد.
نقش، شهر/محله، نوع شیفت، نوع همکاری، مبلغ یا درصد سهم، و عنوان را در صورت وجود استخراج کن.
فقط با یک شیء JSON پاسخ بده با کلیدهای:
decision (approve|reject|review)، confidence (0-100)، reason (فارسی کوتاه)،
kind (shift|job)، role، city، district، shiftType (day|evening|night|oncall)،
employmentType (fulltime|parttime|contract|plan)، payAmount (عدد تومان یا null)،
sharePercent (0-100 یا null)، title، facilityName.
""";
}