using System.Net.Http;
using System.Net.Http.Headers;
using foodmarket.Pos.Core.Auth;
using foodmarket.Pos.Core.Data;
using foodmarket.Pos.Core.Hardware;
using foodmarket.Pos.Core.Sync;
using foodmarket.Pos.Hardware;
using Refit;
namespace foodmarket.Pos.Services;
/// Лёгкий контейнер сервисов кассы (без тяжёлого DI — быстрый старт).
/// Держит локальное хранилище, http-клиент, авторизацию, Refit-API (после входа),
/// сервис синхронизации и оборудование (печать/весы/фискализация-заглушка).
public sealed class AppServices
{
public PosStore Store { get; }
public HttpClient Http { get; }
public AuthService Auth { get; }
public ITokenStore Tokens { get; }
public PosSession? Session { get; set; }
public IServerApi? Api { get; private set; }
public SyncService? Sync { get; private set; }
public IReceiptPrinter Printer { get; set; } = new PdfReceiptPrinter(); // запасной по умолчанию; ESC/POS выбирается в настройках
public IScale Scale { get; set; } = new ManualScale();
public IFiscalProvider Fiscal { get; } = new NullFiscalProvider();
/// Монитор весов: опрос по требованию, мягкая деградация на ручной ввод,
/// запись о потере связи один раз. Источник (COM-порт) — из автопоиска/настроек.
public ScaleMonitor Scales { get; } = new ScaleMonitor(AppendScaleLog);
/// Настройки разбора весовых этикеток организации (по умолчанию ВЫКЛ).
/// Касса использует точный штрихкод; этикетки разбирает только если включено.
public foodmarket.Shared.Barcodes.WeightedBarcodeSettings WeightedSettings { get; set; }
= foodmarket.Shared.Barcodes.WeightedBarcodeSettings.Default;
private CheckoutService? _checkout;
/// Оркестратор завершения чека (очередь → push → печать).
public CheckoutService Checkout => _checkout ??= new CheckoutService(this);
public AppServices()
{
var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "food-market-pos");
Directory.CreateDirectory(dir);
Store = new PosStore(Path.Combine(dir, "catalog.db"));
Http = new HttpClient(NewHandler()) { Timeout = TimeSpan.FromSeconds(30) };
Auth = new AuthService(Http);
// Windows — DPAPI (привязка к учётке), иначе файл с правами 0600.
Tokens = OperatingSystem.IsWindows()
? new DpapiTokenStore(FileTokenStore.DefaultPath())
: new FileTokenStore(FileTokenStore.DefaultPath());
Session = Tokens.Load();
if (Session is not null) ConfigureApi(Session);
RestoreScaleFromSettings();
}
// ── Весы ───────────────────────────────────────────────────────────────
private static readonly string ScaleLogPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "food-market-pos", "scale.log");
private static void AppendScaleLog(string msg)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(ScaleLogPath)!);
File.AppendAllText(ScaleLogPath, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} {msg}{Environment.NewLine}");
}
catch { /* логирование весов не должно мешать кассе */ }
}
/// Поднять сохранённый порт весов (если был выбран ранее).
public void RestoreScaleFromSettings()
{
try
{
var port = Store.GetKv("scale_port");
if (string.IsNullOrWhiteSpace(port)) return;
var baud = int.TryParse(Store.GetKv("scale_baud"), out var b) ? b : 9600;
var proto = Store.GetKv("scale_proto") == "Cas" ? ComScale.Proto.Cas : ComScale.Proto.MassaK;
SetScale(port, baud, proto);
}
catch { /* нет сохранённого порта — работаем с ручным вводом */ }
}
/// Назначить весы на конкретный порт и запомнить выбор.
public void SetScale(string port, int baud, ComScale.Proto proto)
{
var scale = new ComScale(port, baud, proto);
Scale = scale;
Scales.SetReader(ct => scale.ReadWeightKgAsync(ct), scale.Name);
Store.SetKv("scale_port", port);
Store.SetKv("scale_baud", baud.ToString());
Store.SetKv("scale_proto", proto.ToString());
}
/// Автопоиск весов по COM-портам; при успехе — запоминаем. Возвращает,
/// нашлись ли весы.
public async Task DiscoverScaleAsync(CancellationToken ct = default)
{
var found = await ScaleDiscovery.DiscoverAsync(AppendScaleLog, ct);
if (found is null) return false;
SetScale(found.Port, found.Baud, found.Proto);
return true;
}
/// Контейнер с готовым хранилищем — для headless UI-тестов/демо
/// (без файлов на диске и без токен-стора).
public AppServices(PosStore store)
{
Store = store;
Http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
Auth = new AuthService(Http);
Tokens = new InMemoryTokenStore();
}
/// Поднять Refit-клиент на базовый URL сервера с Bearer-токеном сессии.
public void ConfigureApi(PosSession session)
{
Session = session;
var client = new HttpClient(new AuthHeaderHandler(() => Session?.AccessToken, NewHandler()))
{
BaseAddress = new Uri(session.ServerUrl.TrimEnd('/')),
Timeout = TimeSpan.FromSeconds(30),
};
Api = RestService.For(client);
Sync = new SyncService(Store, Api);
}
/// HTTP-handler с автоматической декомпрессией (gzip/deflate/brotli):
/// сервер жмёт JSON каталога (Program.cs AddResponseCompression), но без этого
/// флага HttpClient не шлёт Accept-Encoding и каталог едет несжатым. Для
/// первичной загрузки 30 000 товаров это кратно меньше трафика → быстрее старт.
private static HttpClientHandler NewHandler() => new()
{
AutomaticDecompression = System.Net.DecompressionMethods.GZip
| System.Net.DecompressionMethods.Deflate | System.Net.DecompressionMethods.Brotli,
};
}
/// Подставляет Bearer-токен в каждый запрос.
internal sealed class AuthHeaderHandler : DelegatingHandler
{
private readonly Func _token;
public AuthHeaderHandler(Func token, HttpMessageHandler inner) : base(inner) { _token = token; }
protected override Task SendAsync(HttpRequestMessage request, CancellationToken ct)
{
var t = _token();
if (!string.IsNullOrEmpty(t)) request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", t);
return base.SendAsync(request, ct);
}
}