using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;
using Microsoft.Extensions.Options;

public class AnalyticsService : IAnalyticsService
{
    private readonly ElasticsearchClient _elastic;
    private readonly ElasticsearchOptions _options;

    public AnalyticsService(ElasticsearchClient elastic, IOptions<ElasticsearchOptions> options)
    {
        _elastic = elastic;
        _options = options.Value;
    }

    public async Task<object> GetPopulationAsync(string code, decimal threshold, string direction,
        DateTimeOffset? from, DateTimeOffset? to)
    {
        var filters = new List<Action<QueryDescriptor<ObservationDocument>>>
        {
            fq => fq.Term(t => t.Field(o => o.ObservationCode).Value(code))
        };

        if (direction == "above")
            filters.Add(fq => fq.Range(r => r.NumberRange(nr =>
                nr.Field(o => o.Value).Gt((double)threshold))));
        else
            filters.Add(fq => fq.Range(r => r.NumberRange(nr =>
                nr.Field(o => o.Value).Lt((double)threshold))));

        if (from.HasValue)
            filters.Add(fq => fq.Range(r => r.DateRange(dr =>
                dr.Field(o => o.RecordedAt).Gte(from.Value.ToString("O")))));

        if (to.HasValue)
            filters.Add(fq => fq.Range(r => r.DateRange(dr =>
                dr.Field(o => o.RecordedAt).Lte(to.Value.ToString("O")))));

        var resp = await _elastic.SearchAsync<ObservationDocument>(s => s
            .Indices(_options.Indices.Observations)
            .Query(q => q.Bool(b => b.Filter(filters.ToArray())))
            .Aggregations(a => a
                // cardinality: count of distinct patientId values — not a document count.
                // Two observations from the same patient count as one patient.
                .Add("unique_patients", agg => agg.Cardinality(c => c.Field(o => o.PatientId)))
            )
            .Size(0));  // no raw hits — aggregation result only

        if (!resp.IsValidResponse)
            throw new InvalidOperationException("Elasticsearch query failed.");

        var uniquePatients = resp.Aggregations?.GetCardinality("unique_patients")?.Value ?? 0;

        return new
        {
            observationCode = code,
            threshold,
            direction,
            from,
            to,
            uniquePatientCount = uniquePatients
        };
    }

    public async Task<object> GetObservationTrendAsync(Guid encounterId, string code,
        DateTimeOffset? from, DateTimeOffset? to)
    {
        var filters = new List<Action<QueryDescriptor<ObservationDocument>>>
        {
            fq => fq.Term(t => t.Field(o => o.EncounterId).Value(encounterId.ToString())),
            fq => fq.Term(t => t.Field(o => o.ObservationCode).Value(code))
        };

        if (from.HasValue)
            filters.Add(fq => fq.Range(r => r.DateRange(dr =>
                dr.Field(o => o.RecordedAt).Gte(from.Value.ToString("O")))));

        if (to.HasValue)
            filters.Add(fq => fq.Range(r => r.DateRange(dr =>
                dr.Field(o => o.RecordedAt).Lte(to.Value.ToString("O")))));

        var resp = await _elastic.SearchAsync<ObservationDocument>(s => s
            .Indices(_options.Indices.Observations)
            .Query(q => q.Bool(b => b.Filter(filters.ToArray())))
            .Aggregations(a => a
                .Add("trend", agg => agg.DateHistogram(h => h
                        .Field(o => o.RecordedAt)
                        .CalendarInterval(Elastic.Clients.Elasticsearch.Aggregations.CalendarInterval.Hour)
                    )
                )
            )
            .Size(0));

        if (!resp.IsValidResponse)
            throw new InvalidOperationException("Elasticsearch query failed.");

        var buckets = resp.Aggregations?.GetDateHistogram("trend")?.Buckets
            .Select(b => new
            {
                hour = b.KeyAsString,
                avg = (double?)null,
                min = (double?)null,
                max = (double?)null,
                count = b.DocCount
            }) ?? Enumerable.Empty<object>();

        return new { encounterId, observationCode = code, trend = buckets };
    }

    public async Task<object> GetAlertSummaryAsync(string? severity, string? department,
        DateTimeOffset? from, DateTimeOffset? to)
    {
        var filters = new List<Action<QueryDescriptor<ClinicalAlertDocument>>>();

        if (from.HasValue)
            filters.Add(fq => fq.Range(r => r.DateRange(dr =>
                dr.Field(a => a.TriggeredAt).Gte(from.Value.ToString("O")))));

        if (to.HasValue)
            filters.Add(fq => fq.Range(r => r.DateRange(dr =>
                dr.Field(a => a.TriggeredAt).Lte(to.Value.ToString("O")))));

        if (!string.IsNullOrEmpty(severity))
            filters.Add(fq => fq.Term(t => t.Field(a => a.Severity).Value(severity)));

        if (!string.IsNullOrEmpty(department))
            filters.Add(fq => fq.Term(t => t.Field(a => a.Department).Value(department)));

        var resp = await _elastic.SearchAsync<ClinicalAlertDocument>(s => s
            .Indices(_options.Indices.ClinicalAlerts)
            .Query(q => q.Bool(b =>
            {
                if (filters.Count > 0)
                    b.Filter(filters.ToArray());
                else
                    b.Must(m => m.MatchAll(_ => { }));
            }))
            .Aggregations(a => a
                .Add("by_department", agg => agg.Terms(t => t
                        .Field(a => a.Department).Size(50)
                    )
                )
            )
            .Size(0));

        if (!resp.IsValidResponse)
            throw new InvalidOperationException("Elasticsearch query failed.");

        var summary = resp.Aggregations?.GetStringTerms("by_department")?.Buckets
            .Select(deptBucket => new
            {
                department = deptBucket.Key,
                total = deptBucket.DocCount,
                bySeverity = Enumerable.Empty<object>()
            }) ?? Enumerable.Empty<object>();

        return new { from, to, summary };
    }

    public async Task<object> SearchPatientsAsync(string? q, string? department, string? status,
        int page, int pageSize)
    {
        var filters = new List<Action<QueryDescriptor<PatientEncounterDocument>>>();

        if (!string.IsNullOrEmpty(department))
            filters.Add(fq => fq.Term(t => t.Field(p => p.Department).Value(department)));

        if (!string.IsNullOrEmpty(status))
            filters.Add(fq => fq.Term(t => t.Field(p => p.Status).Value(status)));

        var resp = await _elastic.SearchAsync<PatientEncounterDocument>(s => s
            .Indices(_options.Indices.PatientEncounters)
            .Query(q2 => q2
                .Bool(b =>
                {
                    if (!string.IsNullOrWhiteSpace(q))
                    {
                        b.Should(
                            // MRN: exact keyword match — boosted because MRN lookup is
                            // the most common search pattern for clinicians who know the number
                            s2 => s2.Term(t => t.Field(p => p.Mrn).Value(q).Boost(3)),
                            // Patient name: full-text across the analyzed field
                            s2 => s2.Match(m => m.Field(p => p.PatientName).Query(q)),
                            // Department: exact filter match included in should for relevance scoring
                            s2 => s2.Term(t => t.Field(p => p.Department).Value(q))
                        );
                        b.MinimumShouldMatch(1);
                    }
                    else
                    {
                        b.Must(m => m.MatchAll(_ => { }));
                    }

                    if (filters.Count > 0)
                        b.Filter(filters.ToArray());
                })
            )
            .From((page - 1) * pageSize)
            .Size(pageSize));

        if (!resp.IsValidResponse)
            throw new InvalidOperationException("Elasticsearch query failed.");

        return new
        {
            total = resp.Total,
            page,
            pageSize,
            data = resp.Documents
        };
    }
}
