using Microsoft.Extensions.Options;
using OpenTelemetry.Metrics;
using Serilog;
using Tiku.Application;
using Tiku.Application.PlatformBilling;
using Tiku.Application.Security;
using Tiku.Application.Storage;
using Tiku.Application.Tenancy;
using Tiku.Infrastructure;
using Tiku.Infrastructure.Assets;
using Tiku.Infrastructure.Observability;
using Tiku.Infrastructure.Security;
using Tiku.Infrastructure.Storage;

namespace Tiku.Worker;

internal static class WorkerDependencyInjection
{
    internal static HostApplicationBuilder AddWorkerServices(this HostApplicationBuilder builder)
    {
        builder.Services.AddSerilog((services, configuration) => configuration
                .ReadFrom.Configuration(builder.Configuration)
                .ReadFrom.Services(services)
                .Enrich.FromLogContext(),
            true);

        var connectionString = builder.Configuration.GetConnectionString("Database") ??
                               builder.Configuration["DATABASE_URL"] ??
                               (builder.Environment.IsDevelopment()
                                   ? $"Host=localhost;Database=tiku;Username={Environment.UserName}"
                                   : throw new InvalidOperationException(
                                       "Database connection is required outside Development. Configure ConnectionStrings:Database or DATABASE_URL."));
        builder.Services.AddApplication();
        builder.Services.AddInfrastructure(connectionString);
        var redisConnectionString =
            builder.Configuration.GetConnectionString("Redis") ?? builder.Configuration["REDIS_URL"];
        builder.Services.AddOptions<AuthorizationCacheOptions>()
            .Bind(builder.Configuration.GetSection(AuthorizationCacheOptions.SectionName));
        if (!string.IsNullOrWhiteSpace(redisConnectionString))
            builder.Services.AddRedisSecurity(redisConnectionString, builder.Environment.EnvironmentName);
        else if (builder.Environment.IsProduction())
            throw new InvalidOperationException(
                "Redis is required in Production for authorization cache invalidation retries.");
        var otlpEndpoint = builder.Configuration["OpenTelemetry:OtlpEndpoint"];
        var telemetry = builder.Services.AddOpenTelemetry().WithMetrics(metrics =>
            metrics.AddMeter(WorkerTelemetry.MeterName, AuthorizationCacheTelemetry.MeterName));
        if (Uri.TryCreate(otlpEndpoint, UriKind.Absolute, out var endpoint))
            telemetry.WithMetrics(metrics => metrics.AddOtlpExporter(options => options.Endpoint = endpoint));
        builder.Services.Configure<ObjectStorageOptions>(
            builder.Configuration.GetSection(ObjectStorageOptions.SectionName));
        builder.Services.Configure<AliyunOssOptions>(builder.Configuration.GetSection(AliyunOssOptions.SectionName));
        builder.Services.PostConfigure<ObjectStorageOptions>(options =>
        {
            options.DefaultProvider = builder.Configuration["STORAGE_DEFAULT_PROVIDER"] ?? options.DefaultProvider;
            options.DefaultBucket = builder.Configuration["STORAGE_DEFAULT_BUCKET"] ?? options.DefaultBucket;
            options.PublicBaseUrl = builder.Configuration["STORAGE_PUBLIC_BASE_URL"] ?? options.PublicBaseUrl;
            options.AllowedMimePrefixes = SplitLegacyList(
                builder.Configuration["STORAGE_ALLOWED_MIME_PREFIXES"],
                options.AllowedMimePrefixes);
            options.AllowedMimeTypes = SplitLegacyList(
                builder.Configuration["STORAGE_ALLOWED_MIME_TYPES"],
                options.AllowedMimeTypes);
            options.RequireTenantPrefix = bool.TryParse(
                builder.Configuration["STORAGE_REQUIRE_TENANT_PREFIX"],
                out var requireTenantPrefix)
                ? requireTenantPrefix
                : options.RequireTenantPrefix;
            options.MaxUploadBytes = long.TryParse(
                builder.Configuration["STORAGE_MAX_UPLOAD_BYTES"],
                out var maxUploadBytes)
                ? maxUploadBytes
                : options.MaxUploadBytes;
        });
        builder.Services.PostConfigure<AliyunOssOptions>(options =>
        {
            options.Region = builder.Configuration["ALIYUN_OSS_REGION"] ?? options.Region;
            options.Endpoint = builder.Configuration["ALIYUN_OSS_ENDPOINT"] ?? options.Endpoint;
            options.AccessKeyId = builder.Configuration["ALIYUN_OSS_ACCESS_KEY_ID"] ?? options.AccessKeyId;
            options.AccessKeySecret = builder.Configuration["ALIYUN_OSS_ACCESS_KEY_SECRET"] ?? options.AccessKeySecret;
            options.SecurityToken = builder.Configuration["ALIYUN_OSS_STS_TOKEN"] ?? options.SecurityToken;
            options.UseInternalEndpoint = bool.TryParse(
                builder.Configuration["ALIYUN_OSS_INTERNAL"],
                out var useInternalEndpoint)
                ? useInternalEndpoint
                : options.UseInternalEndpoint;
        });
        builder.Services.AddOptions<ObjectStorageOptions>()
            .Validate(
                options => !builder.Environment.IsProduction() ||
                           options.DefaultProvider == ObjectStorageProviders.AliyunOss,
                "Production managed storage must use the configured Aliyun OSS provider.")
            .ValidateOnStart();
        builder.Services.AddOptions<AliyunOssOptions>()
            .Validate<IOptions<ObjectStorageOptions>>(
                (aliyun, storage) =>
                    !builder.Environment.IsProduction() ||
                    storage.Value.DefaultProvider != ObjectStorageProviders.AliyunOss ||
                    aliyun.IsConfigured,
                "Aliyun OSS credentials and region or endpoint are required when it is the default provider.")
            .ValidateOnStart();
        builder.Services.AddOptions<ClamAvOptions>()
            .Bind(builder.Configuration.GetSection(ClamAvOptions.SectionName))
            .Validate(ClamAvOptions.BeValid, "ClamAV settings are invalid.")
            .Validate<IOptions<ObjectStorageOptions>>(
                (clamAv, storage) => clamAv.StreamMaxLength >= storage.Value.MaxUploadBytes,
                "ClamAV StreamMaxLength must cover the storage max upload size.")
            .ValidateOnStart();
        builder.Services.AddOptions<WorkerOptions>()
            .Bind(builder.Configuration.GetSection(WorkerOptions.SectionName))
            .Validate(WorkerOptions.BeValid, "Worker settings are invalid.")
            .ValidateOnStart();
        builder.Services.AddOptions<DomainLifecycleOptions>()
            .Bind(builder.Configuration.GetSection("TenantDomains"));
        builder.Services.AddOptions<SaasSubscriptionLifecycleOptions>()
            .Bind(builder.Configuration.GetSection("SaasSubscriptions"));
        builder.Services.AddOptions<FeatureUsageReconciliationOptions>()
            .Bind(builder.Configuration.GetSection("FeatureUsageReconciliation"));
        builder.Services.AddOptions<CommercialBillingOptions>()
            .Bind(builder.Configuration.GetSection("CommercialBilling"))
            .Validate(options => options.BatchSize is >= 1 and <= 1000, "Commercial billing batch size is invalid.")
            .ValidateOnStart();
        builder.Services.AddSingleton<IPeriodicProcessorLock, PostgresPeriodicProcessorLock>();
        builder.Services.AddSingleton<WorkerStateReporter>();
        builder.Services.AddHostedService<TenantDomainWorker>();
        builder.Services.AddHostedService<SaasSubscriptionWorker>();
        builder.Services.AddHostedService<FeatureUsageWorker>();
        builder.Services.AddHostedService<BackgroundJobsWorker>();
        builder.Services.AddHostedService<AuthorizationCacheInvalidationWorker>();
        builder.Services.AddHostedService<CommercialBillingWorker>();
        builder.Services.AddHostedService<PlatformApprovalWorker>();
        return builder;
    }

    private static string[] SplitLegacyList(string? value, string[] fallback)
    {
        return string.IsNullOrWhiteSpace(value)
            ? fallback
            : value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
    }
}