from datetime import datetime
import json
import logging
import re
from typing import (
    Dict,
    Iterable,
    List,
    Literal,
    NamedTuple,
    TypeAlias,
    TypeGuard,
    get_args,
)

import pandas as pd
import requests

from epx.run.exec.cloud.auth import platform_api_headers
from epx.run.exec.cloud.config import api_base_url
from epx.run.exec.cloud.strategy import ForbiddenResponse, UnauthorizedUserError
from epx.run.run import Run

logger = logging.getLogger(__name__)

StatusName = Literal["NOT STARTED", "RUNNING", "ERROR", "DONE"]
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR"]


class LogItem(NamedTuple):
    """An individual entry in the logs generated by FRED.

    Attributes
    ----------
    level : LogLevel
        The log level of the message, e.g. `INFO`, `ERROR`, etc.
    time : datetime
        The time that the message was reported at.
    message : str
        The log message.
    """

    level: LogLevel
    time: datetime
    message: str


RunWithId: TypeAlias = tuple[int, Run]


class JobStatus:
    def __init__(self, job_name: str, _run_with_ids: Iterable[RunWithId]):
        self.job_name = job_name
        self._run_with_ids = _run_with_ids
        self._log_re = re.compile(
            r"^\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)\] ([A-Z]*): (.*)$"
        )

    @property
    def logs(self) -> pd.DataFrame:
        """Return a collection of log entries output by FRED.

        Returns
        -------
        pd.DataFrame
            Collection of individual log entries generated by FRED during the
            run.
        """

        def process_log_line(line: str) -> LogItem:
            m = self._log_re.match(line)
            if m is None:
                raise ValueError(f"Invalid logline: {line}")
            level = m.group(2)
            assert self._is_valid_log_level(level)
            return LogItem(
                level,
                datetime.strptime(m.group(1), "%Y-%m-%dT%H:%M:%S.%fZ"),
                m.group(3),
            )

        data_frames: List[pd.DataFrame] = []
        for run_id, run in self._run_with_ids:
            try:
                p = run.output_dir / "logs.txt"
                with open(p, "r") as f:
                    log_records = tuple(
                        process_log_line(line) for line in f.readlines()
                    )
            except FileNotFoundError:
                log_records = tuple()
            logs = pd.DataFrame.from_records(
                log_records, columns=["level", "time", "message"]
            )
            data_frames.append(logs.assign(run_id=run_id))

        log_cols = ["run_id", "level", "time", "message"]
        if len(data_frames) != 0:
            return pd.concat(data_frames).loc[:, log_cols]
        return pd.DataFrame.from_records(tuple(), columns=log_cols)

    @property
    def name(self) -> StatusName:
        """Return a string summarizing the job status.

        Returns
        -------
        Status
            A string indicating the status of the job, one of: `"NOT STARTED"`,
            `"RUNNING"`, `"ERROR"`, or `"DONE"`.
        """

        pod_phase_map: Dict[str | None, StatusName] = {
            "Pending": "NOT STARTED",
            "Running": "RUNNING",
            "Succeeded": "DONE",
            "Failed": "ERROR",
            "Unknown": "ERROR",
            "": "NOT STARTED",
            None: "NOT STARTED",
        }

        # call api here to get StatusName
        endpoint_url = f"{api_base_url()}/runs"
        # Get request for a run to be executed to FRED Cloud API
        response = requests.get(
            endpoint_url,
            headers=platform_api_headers(),
            params={"job_name": str(self.job_name)},
        )

        # Check HTTP response status code and raise exceptions as appropriate
        if not response.ok:
            if response.status_code == requests.codes.forbidden:
                raise UnauthorizedUserError(
                    ForbiddenResponse.model_validate_json(response.text).description
                )
            else:
                raise RuntimeError(f"FRED Cloud error code: {response.status_code}")

        response_payload = json.loads(response.text)
        logger.debug(f"Payload: {response.text}")

        runs = response_payload["runs"]
        if len(runs) == 0:
            return "NOT STARTED"

        status_names = set(pod_phase_map[run["podPhase"]] for run in runs)

        if all(sn == "NOT STARTED" for sn in status_names):
            return "NOT STARTED"
        if "ERROR" in status_names:
            return "ERROR"
        if "RUNNING" in status_names:
            return "RUNNING"
        return "DONE"

    @staticmethod
    def _is_valid_log_level(level: str) -> TypeGuard[LogLevel]:
        """Helper method for validating that a string is a valid log level."""
        return level in get_args(LogLevel)

    def __repr__(self) -> str:
        return f"JobStatus({self.job_name})"

    def __str__(self) -> str:
        return self.name
