import re
import time

from datetime import datetime
from moto.core import BaseBackend, BaseModel
from moto.core.utils import iso_8601_datetime_without_milliseconds, BackendDict
from moto.utilities.tagging_service import TaggingService
from .exceptions import (
    S3BucketDoesNotExistException,
    InsufficientSnsTopicPolicyException,
    TrailNameTooLong,
    TrailNameTooShort,
    TrailNameNotStartingCorrectly,
    TrailNameNotEndingCorrectly,
    TrailNameInvalidChars,
    TrailNotFoundException,
)


def datetime2int(date):
    return int(time.mktime(date.timetuple()))


class TrailStatus(object):
    def __init__(self):
        self.is_logging = False
        self.latest_delivery_time = ""
        self.latest_delivery_attempt = ""
        self.start_logging_time = None
        self.started = None
        self.stopped = None

    def start_logging(self):
        self.is_logging = True
        self.started = datetime.utcnow()
        self.latest_delivery_time = datetime2int(datetime.utcnow())
        self.latest_delivery_attempt = iso_8601_datetime_without_milliseconds(
            datetime.utcnow()
        )

    def stop_logging(self):
        self.is_logging = False
        self.stopped = datetime.utcnow()

    def description(self):
        if self.is_logging:
            self.latest_delivery_time = datetime2int(datetime.utcnow())
            self.latest_delivery_attempt = iso_8601_datetime_without_milliseconds(
                datetime.utcnow()
            )
        desc = {
            "IsLogging": self.is_logging,
            "LatestDeliveryAttemptTime": self.latest_delivery_attempt,
            "LatestNotificationAttemptTime": "",
            "LatestNotificationAttemptSucceeded": "",
            "LatestDeliveryAttemptSucceeded": "",
            "TimeLoggingStarted": "",
            "TimeLoggingStopped": "",
        }
        if self.started:
            desc["StartLoggingTime"] = datetime2int(self.started)
            desc["TimeLoggingStarted"] = iso_8601_datetime_without_milliseconds(
                self.started
            )
            desc["LatestDeliveryTime"] = self.latest_delivery_time
        if self.stopped:
            desc["StopLoggingTime"] = datetime2int(self.stopped)
            desc["TimeLoggingStopped"] = iso_8601_datetime_without_milliseconds(
                self.stopped
            )
        return desc


class Trail(BaseModel):
    def __init__(
        self,
        account_id,
        region_name,
        trail_name,
        bucket_name,
        s3_key_prefix,
        sns_topic_name,
        is_global,
        is_multi_region,
        log_validation,
        is_org_trail,
        cw_log_group_arn,
        cw_role_arn,
        kms_key_id,
    ):
        self.account_id = account_id
        self.region_name = region_name
        self.trail_name = trail_name
        self.bucket_name = bucket_name
        self.s3_key_prefix = s3_key_prefix
        self.sns_topic_name = sns_topic_name
        self.is_multi_region = is_multi_region
        self.log_validation = log_validation
        self.is_org_trail = is_org_trail
        self.include_global_service_events = is_global
        self.cw_log_group_arn = cw_log_group_arn
        self.cw_role_arn = cw_role_arn
        self.kms_key_id = kms_key_id
        self.check_name()
        self.check_bucket_exists()
        self.check_topic_exists()
        self.status = TrailStatus()
        self.event_selectors = list()
        self.advanced_event_selectors = list()
        self.insight_selectors = list()

    @property
    def arn(self):
        return f"arn:aws:cloudtrail:{self.region_name}:{self.account_id}:trail/{self.trail_name}"

    @property
    def topic_arn(self):
        if self.sns_topic_name:
            return f"arn:aws:sns:{self.region_name}:{self.account_id}:{self.sns_topic_name}"
        return None

    def check_name(self):
        if len(self.trail_name) < 3:
            raise TrailNameTooShort(actual_length=len(self.trail_name))
        if len(self.trail_name) > 128:
            raise TrailNameTooLong(actual_length=len(self.trail_name))
        if not re.match("^[0-9a-zA-Z]{1}.+$", self.trail_name):
            raise TrailNameNotStartingCorrectly()
        if not re.match(r".+[0-9a-zA-Z]{1}$", self.trail_name):
            raise TrailNameNotEndingCorrectly()
        if not re.match(r"^[.\-_0-9a-zA-Z]+$", self.trail_name):
            raise TrailNameInvalidChars()

    def check_bucket_exists(self):
        from moto.s3.models import s3_backends

        try:
            s3_backends[self.account_id]["global"].get_bucket(self.bucket_name)
        except Exception:
            raise S3BucketDoesNotExistException(
                f"S3 bucket {self.bucket_name} does not exist!"
            )

    def check_topic_exists(self):
        if self.sns_topic_name:
            from moto.sns import sns_backends

            sns_backend = sns_backends[self.account_id][self.region_name]
            try:
                sns_backend.get_topic(self.topic_arn)
            except Exception:
                raise InsufficientSnsTopicPolicyException(
                    "SNS Topic does not exist or the topic policy is incorrect!"
                )

    def start_logging(self):
        self.status.start_logging()

    def stop_logging(self):
        self.status.stop_logging()

    def put_event_selectors(self, event_selectors, advanced_event_selectors):
        if event_selectors:
            self.event_selectors = event_selectors
        elif advanced_event_selectors:
            self.event_selectors = []
            self.advanced_event_selectors = advanced_event_selectors

    def get_event_selectors(self):
        return self.event_selectors, self.advanced_event_selectors

    def put_insight_selectors(self, insight_selectors):
        self.insight_selectors.extend(insight_selectors)

    def get_insight_selectors(self):
        return self.insight_selectors

    def update(
        self,
        s3_bucket_name,
        s3_key_prefix,
        sns_topic_name,
        include_global_service_events,
        is_multi_region_trail,
        enable_log_file_validation,
        is_organization_trail,
        cw_log_group_arn,
        cw_role_arn,
        kms_key_id,
    ):
        if s3_bucket_name is not None:
            self.bucket_name = s3_bucket_name
        if s3_key_prefix is not None:
            self.s3_key_prefix = s3_key_prefix
        if sns_topic_name is not None:
            self.sns_topic_name = sns_topic_name
        if include_global_service_events is not None:
            self.include_global_service_events = include_global_service_events
        if is_multi_region_trail is not None:
            self.is_multi_region = is_multi_region_trail
        if enable_log_file_validation is not None:
            self.log_validation = enable_log_file_validation
        if is_organization_trail is not None:
            self.is_org_trail = is_organization_trail
        if cw_log_group_arn is not None:
            self.cw_log_group_arn = cw_log_group_arn
        if cw_role_arn is not None:
            self.cw_role_arn = cw_role_arn
        if kms_key_id is not None:
            self.kms_key_id = kms_key_id

    def short(self):
        return {
            "Name": self.trail_name,
            "TrailARN": self.arn,
            "HomeRegion": self.region_name,
        }

    def description(self, include_region=False):
        desc = {
            "Name": self.trail_name,
            "S3BucketName": self.bucket_name,
            "IncludeGlobalServiceEvents": self.include_global_service_events,
            "IsMultiRegionTrail": self.is_multi_region,
            "TrailARN": self.arn,
            "LogFileValidationEnabled": self.log_validation,
            "IsOrganizationTrail": self.is_org_trail,
            "HasCustomEventSelectors": False,
            "HasInsightSelectors": False,
            "CloudWatchLogsLogGroupArn": self.cw_log_group_arn,
            "CloudWatchLogsRoleArn": self.cw_role_arn,
            "KmsKeyId": self.kms_key_id,
        }
        if self.s3_key_prefix is not None:
            desc["S3KeyPrefix"] = self.s3_key_prefix
        if self.sns_topic_name is not None:
            desc["SnsTopicName"] = self.sns_topic_name
            desc["SnsTopicARN"] = self.topic_arn
        if include_region:
            desc["HomeRegion"] = self.region_name
        return desc


class CloudTrailBackend(BaseBackend):
    """Implementation of CloudTrail APIs."""

    def __init__(self, region_name, account_id):
        super().__init__(region_name, account_id)
        self.trails = dict()
        self.tagging_service = TaggingService(tag_name="TagsList")

    def create_trail(
        self,
        name,
        bucket_name,
        s3_key_prefix,
        sns_topic_name,
        is_global,
        is_multi_region,
        log_validation,
        is_org_trail,
        cw_log_group_arn,
        cw_role_arn,
        kms_key_id,
        tags_list,
    ):
        trail = Trail(
            self.account_id,
            self.region_name,
            name,
            bucket_name,
            s3_key_prefix,
            sns_topic_name,
            is_global,
            is_multi_region,
            log_validation,
            is_org_trail,
            cw_log_group_arn,
            cw_role_arn,
            kms_key_id,
        )
        self.trails[name] = trail
        self.tagging_service.tag_resource(trail.arn, tags_list)
        return trail

    def get_trail(self, name_or_arn):
        if len(name_or_arn) < 3:
            raise TrailNameTooShort(actual_length=len(name_or_arn))
        if name_or_arn in self.trails:
            return self.trails[name_or_arn]
        for trail in self.trails.values():
            if trail.arn == name_or_arn:
                return trail
        raise TrailNotFoundException(account_id=self.account_id, name=name_or_arn)

    def get_trail_status(self, name):
        if len(name) < 3:
            raise TrailNameTooShort(actual_length=len(name))
        trail_name = next(
            (
                trail.trail_name
                for trail in self.trails.values()
                if trail.trail_name == name or trail.arn == name
            ),
            None,
        )
        if not trail_name:
            # This particular method returns the ARN as part of the error message
            arn = (
                f"arn:aws:cloudtrail:{self.region_name}:{self.account_id}:trail/{name}"
            )
            raise TrailNotFoundException(account_id=self.account_id, name=arn)
        trail = self.trails[trail_name]
        return trail.status

    def describe_trails(self, include_shadow_trails):
        all_trails = []
        if include_shadow_trails:
            current_account = cloudtrail_backends[self.account_id]
            for backend in current_account.values():
                all_trails.extend(backend.trails.values())
        else:
            all_trails.extend(self.trails.values())
        return all_trails

    def list_trails(self):
        return self.describe_trails(include_shadow_trails=True)

    def start_logging(self, name):
        trail = self.trails[name]
        trail.start_logging()

    def stop_logging(self, name):
        trail = self.trails[name]
        trail.stop_logging()

    def delete_trail(self, name):
        if name in self.trails:
            del self.trails[name]

    def update_trail(
        self,
        name,
        s3_bucket_name,
        s3_key_prefix,
        sns_topic_name,
        include_global_service_events,
        is_multi_region_trail,
        enable_log_file_validation,
        is_organization_trail,
        cw_log_group_arn,
        cw_role_arn,
        kms_key_id,
    ):
        trail = self.get_trail(name_or_arn=name)
        trail.update(
            s3_bucket_name=s3_bucket_name,
            s3_key_prefix=s3_key_prefix,
            sns_topic_name=sns_topic_name,
            include_global_service_events=include_global_service_events,
            is_multi_region_trail=is_multi_region_trail,
            enable_log_file_validation=enable_log_file_validation,
            is_organization_trail=is_organization_trail,
            cw_log_group_arn=cw_log_group_arn,
            cw_role_arn=cw_role_arn,
            kms_key_id=kms_key_id,
        )
        return trail

    def put_event_selectors(
        self, trail_name, event_selectors, advanced_event_selectors
    ):
        trail = self.get_trail(trail_name)
        trail.put_event_selectors(event_selectors, advanced_event_selectors)
        trail_arn = trail.arn
        return trail_arn, event_selectors, advanced_event_selectors

    def get_event_selectors(self, trail_name):
        trail = self.get_trail(trail_name)
        event_selectors, advanced_event_selectors = trail.get_event_selectors()
        return trail.arn, event_selectors, advanced_event_selectors

    def add_tags(self, resource_id, tags_list):
        self.tagging_service.tag_resource(resource_id, tags_list)

    def remove_tags(self, resource_id, tags_list):
        self.tagging_service.untag_resource_using_tags(resource_id, tags_list)

    def list_tags(self, resource_id_list):
        """
        Pagination is not yet implemented
        """
        resp = [{"ResourceId": r_id} for r_id in resource_id_list]
        for item in resp:
            item["TagsList"] = self.tagging_service.list_tags_for_resource(
                item["ResourceId"]
            )["TagsList"]
        return resp

    def put_insight_selectors(self, trail_name, insight_selectors):
        trail = self.get_trail(trail_name)
        trail.put_insight_selectors(insight_selectors)
        return trail.arn, insight_selectors

    def get_insight_selectors(self, trail_name):
        trail = self.get_trail(trail_name)
        return trail.arn, trail.get_insight_selectors()


cloudtrail_backends = BackendDict(CloudTrailBackend, "cloudtrail")
