import time
from collections import defaultdict
from threading import Lock

from opentelemetry import metrics
from opentelemetry.sdk.metrics import Histogram
from opentelemetry.sdk.metrics.export import AggregationTemporality, HistogramDataPoint, InMemoryMetricReader
from rich.table import Table

from aiolocust import otel
from aiolocust.datatypes import Request, RequestEntry

MAX_ERROR_KEYS = 200

reader = InMemoryMetricReader(
    preferred_temporality={
        Histogram: AggregationTemporality.DELTA,
    }
)

otel.setup_trace_exporters()
otel.setup_meter_provider([reader])
meter = metrics.get_meter("locust")
ttlb_histogram = meter.create_histogram("http.client.duration")
error_counter = defaultdict(int)
error_counter_lock = Lock()


def record_error(message: str):
    with error_counter_lock:
        if message not in error_counter and len(error_counter) >= MAX_ERROR_KEYS:
            message = "OTHER"
        error_counter[message] += 1


def request(req: Request):
    attributes = {
        "http.url": req.url,
        # the rest of these remain to be implemented
        # http.method=GET,
        # http.host=localhost,
        # net.peer.name=localhost,
        # net.peer.port=8080,
        # http.status_code=200}
    }
    if req.error:
        attributes["error.type"] = req.error.__class__.__name__
        record_error(str(req.error) or req.error.__class__.__name__)
    ttlb_histogram.record(req.ttlb, attributes=attributes)


def make_row(name: str, re: RequestEntry, start, end) -> list[str]:
    return [
        name,
        str(re.count),
        f"{re.errorcount} ({re.error_percentage:2.1f}%)",
        f"{re.avg_ttlb_ms:4.1f}ms",
        f"{re.max_ttlb_ms:4.1f}ms",
        f"{re.rate(start, end):.2f}/s",
    ]


class StatsFormatter:
    def __init__(self):
        self.start_time = time.time()
        self.last_time = self.start_time
        self.aggregate: dict[str, RequestEntry] = defaultdict(RequestEntry)
        # clear reader, in case this is not the first Stats object
        _ = reader.get_metrics_data()
        error_counter.clear()

    def _get_entries(self) -> dict[str, RequestEntry]:
        metrics_data = reader.get_metrics_data()
        entries: dict[str, RequestEntry] = defaultdict(RequestEntry)
        for resource_metric in metrics_data.resource_metrics if metrics_data else []:
            for scope_metric in resource_metric.scope_metrics:
                for metric in scope_metric.metrics:
                    for point in metric.data.data_points:
                        if not point.attributes:
                            raise Exception(f"A data point had no attributes, that should never happen. Point: {point}")
                        if not isinstance(point, HistogramDataPoint):
                            continue  # maybe we should log this, but we don't want to kill performance either
                        if name := point.attributes.get("http.url"):
                            entries[str(name)] += RequestEntry(
                                point.count,
                                point.count if point.attributes.get("error.type") else 0,
                                point.sum,
                                point.max,
                            )
                        else:
                            pass  # print(f"Unknown point {point}")

        return entries

    def _get_rows(self, final_summary) -> list[list[str]]:
        table: list[list[str]] = []
        summary_table: list[list[str]] = []
        now = time.time()

        entries = self._get_entries()
        total = RequestEntry()

        for url, re in entries.items():
            self.aggregate[url] += re
            total += re
            table.append(make_row(url, re, self.last_time, now))
        table.append(make_row("Total", total, self.last_time, now))

        self.last_time = now

        if not final_summary:
            return table

        summary_total = RequestEntry()

        for url, re in self.aggregate.items():
            summary_total += re
            summary_table.append(make_row(url, re, self.start_time, now))
        summary_table.append(make_row("Total", summary_total, self.start_time, now))

        return summary_table

    def get_table(self, final_summary=False):
        table = Table(show_edge=False)
        table.add_column("Name", max_width=30)
        table.add_column("Count", justify="right")
        table.add_column("Failures", justify="right")
        table.add_column("Avg", justify="right")
        table.add_column("Max", justify="right")
        table.add_column("Rate", justify="right")

        for row in self._get_rows(final_summary):
            table.add_row(*row)

        if final_summary:
            table.title = "Summary"

        return table

    def get_error_table(self):
        error_table = Table(show_edge=False)
        error_table.add_column("Count")
        error_table.add_column("Error")

        for key, count in sorted(error_counter.items(), key=lambda item: item[1], reverse=True):
            error_table.add_row(str(count), key)

        return error_table
