from __future__ import annotations

import datetime
import sys
from dataclasses import dataclass
from typing import Dict
from typing import List
from typing import Optional

import click
import pendulum

from tecton import tecton_context
from tecton._internals import metadata_service
from tecton.cli import cli_utils
from tecton.cli import printer
from tecton.cli.cli_utils import cli_indent
from tecton.cli.command import TectonGroup
from tecton_core.errors import TectonNotFoundError
from tecton_core.id_helper import IdHelper
from tecton_core.specs.utils import get_timestamp_field_or_none
from tecton_proto.data import state_update__client_pb2 as state_update_pb2
from tecton_proto.metadataservice import metadata_service__client_pb2 as metadata_service_pb2


def _format_date(datetime: Optional[pendulum.DateTime]):
    if datetime:
        return datetime.strftime("%Y-%m-%d %H:%M:%S %Z")


@dataclass
class IntegrationTestSummaries:
    # This is a map of FeatureViewName to the list of integration test statuses for all integration tests
    #   run for that FeatureView as part of the Plan Integration Tests.
    statuses: Dict[str, List[state_update_pb2.IntegrationTestJobStatus]]

    def has_integration_tests(self):
        return bool(self.all_test_statuses())

    def all_test_statuses(self):
        all_test_statuses = []
        for _, status_list in self.statuses.items():
            all_test_statuses.extend(status_list)
        return all_test_statuses

    @staticmethod
    def _summarize_status(integration_status_list: List) -> str:
        """Given a list of integration test statuses, summarize the state of the entire bunch."""
        if not integration_status_list:
            return "No Tests"
        elif all(
            integration_status == state_update_pb2.IntegrationTestJobStatus.JOB_STATUS_SUCCEED
            for integration_status in integration_status_list
        ):
            return "Succeeded"
        elif any(
            integration_status == state_update_pb2.IntegrationTestJobStatus.JOB_STATUS_FAILED
            for integration_status in integration_status_list
        ):
            return "Failed"
        elif any(
            integration_status == state_update_pb2.IntegrationTestJobStatus.JOB_STATUS_CANCELLED
            for integration_status in integration_status_list
        ):
            return "Canceled"
        elif any(
            integration_status == state_update_pb2.IntegrationTestJobStatus.JOB_STATUS_RUNNING
            for integration_status in integration_status_list
        ):
            return "Running"
        elif any(
            integration_status == state_update_pb2.IntegrationTestJobStatus.JOB_STATUS_NOT_STARTED
            for integration_status in integration_status_list
        ):
            return "Not Started"
        else:
            return "Unknown Status"

    def summarize_status_for_all_tests(self):
        return self._summarize_status(self.all_test_statuses())

    def summarize_status_by_fv(self):
        return {fv_name: self._summarize_status(status_list) for fv_name, status_list in self.statuses.items()}

    @classmethod
    def from_protobuf(cls, successful_plan_output: state_update_pb2.SuccessfulPlanOutput):
        statuses = {}
        for test_summary in successful_plan_output.test_summaries:
            test_job_statuses = [job_summary.status for job_summary in test_summary.job_summaries]
            statuses[test_summary.feature_view_name] = test_job_statuses
        return cls(statuses=statuses)


@dataclass
class PlanListItem:
    plan_id: str
    applied_by: Optional[str]
    applied_at: Optional[pendulum.DateTime]
    created_by: str
    created_at: pendulum.DateTime
    workspace: str
    sdk_version: str
    integration_test_statuses: IntegrationTestSummaries

    @property
    def applied(self):
        if bool(self.applied_by):
            return "Applied"
        else:
            return "Created"

    @classmethod
    def from_proto(cls, state_update_entry: state_update_pb2.StateUpdateEntry):
        applied_by = cli_utils.display_principal(state_update_entry.applied_by_principal, state_update_entry.applied_by)
        applied_at = get_timestamp_field_or_none(state_update_entry, "applied_at")
        created_at = get_timestamp_field_or_none(state_update_entry, "created_at")
        return cls(
            # commit_id is called plan_id in public facing UX. Re-aliasing here.
            plan_id=state_update_entry.commit_id,
            applied_by=applied_by,
            applied_at=applied_at,
            created_by=state_update_entry.created_by,
            created_at=created_at,
            workspace=state_update_entry.workspace or "prod",
            sdk_version=state_update_entry.sdk_version,
            integration_test_statuses=IntegrationTestSummaries.from_protobuf(state_update_entry.successful_plan_output),
        )


@dataclass
class PlanSummary:
    applied_at: Optional[datetime.datetime]
    applied_by: Optional[str]
    applied: bool
    created_at: datetime.datetime
    created_by: str
    workspace: str
    sdk_version: str
    plan_url: str
    integration_test_statuses: IntegrationTestSummaries

    @classmethod
    def from_proto(cls, query_state_update_response: metadata_service_pb2.QueryStateUpdateResponseV2):
        applied_at = get_timestamp_field_or_none(query_state_update_response, "applied_at")
        applied_by = cli_utils.display_principal(
            query_state_update_response.applied_by_principal, query_state_update_response.applied_by
        )
        applied = bool(applied_at)
        created_at = get_timestamp_field_or_none(query_state_update_response, "created_at")
        return cls(
            applied=applied,
            applied_at=applied_at,
            applied_by=applied_by,
            created_at=created_at,
            created_by=query_state_update_response.created_by,
            workspace=query_state_update_response.workspace or "prod",
            sdk_version=query_state_update_response.sdk_version,
            plan_url=query_state_update_response.successful_plan_output.plan_url,
            integration_test_statuses=IntegrationTestSummaries.from_protobuf(
                query_state_update_response.successful_plan_output
            ),
        )


def get_plans_list_items(workspace: str, limit: int):
    request = metadata_service_pb2.GetStateUpdatePlanListRequest(workspace=workspace, limit=limit)
    response = metadata_service.instance().GetStateUpdatePlanList(request)
    return [PlanListItem.from_proto(entry) for entry in response.entries]


def get_plan(workspace: str, plan_id: str):
    plan_id = IdHelper.from_string(plan_id)
    request = metadata_service_pb2.QueryStateUpdateRequestV2(
        state_id=plan_id, workspace=workspace, no_color=True, json_output=False, suppress_warnings=False
    )
    try:
        response = metadata_service.instance().QueryStateUpdateV2(request)
    except TectonNotFoundError:
        printer.safe_print(
            f'Plan id "{plan_id}" not found in workspace {workspace}. Run `tecton plan-info list` to see list of '
            f"available plans."
        )
        sys.exit(1)
    return PlanSummary.from_proto(response.response_proto)


@click.group("plan-info", cls=TectonGroup)
def plan_info():
    r"""View info about plans."""


@plan_info.command(uses_workspace=True)
@click.option("--limit", default=10, type=int, help="Number of log entries to return.")
def list(limit):
    """List previous Tecton plans in this workspace."""
    workspace = tecton_context.get_current_workspace()
    entries = get_plans_list_items(workspace, limit)
    table_rows = [
        (
            entry.plan_id,
            entry.applied,
            entry.integration_test_statuses.summarize_status_for_all_tests(),
            entry.created_by,
            _format_date(entry.created_at),
            entry.sdk_version,
        )
        for entry in entries
    ]
    cli_utils.display_table(
        ["Plan Id", "Plan Status", "Test Status", "Created by", "Creation Date", "SDK Version"], table_rows
    )


@plan_info.command()
@click.argument("plan-id", required=True)
def show(plan_id):
    """Show details of a Tecton Plan"""
    workspace = tecton_context.get_current_workspace()
    plan = get_plan(plan_id=plan_id, workspace=workspace)
    printer.safe_print(f"Showing current status for Plan {plan_id}")
    printer.safe_print()
    printer.safe_print(f"Plan Started At: {_format_date(plan.created_at)}")
    printer.safe_print(f"Plan Created By: {plan.created_by}")
    printer.safe_print(f"Plan Applied: {plan.applied}")
    if plan.applied:
        printer.safe_print(f"Applied At: {_format_date(plan.applied_at)}")
        printer.safe_print(f"Applied By: {plan.applied_by}")

    test_statuses = plan.integration_test_statuses
    printer.safe_print(f"Integration Test Status: {test_statuses.summarize_status_for_all_tests()}")
    if test_statuses.has_integration_tests():
        printer.safe_print("Status by Feature View:")
        for fv, status in test_statuses.summarize_status_by_fv().items():
            printer.safe_print(f"{cli_indent()}{fv}: {status}")
    printer.safe_print()
    printer.safe_print(f"View your plan in the Web UI: {plan.plan_url}")
    printer.safe_print()
