"""AWS provider config schema.

Bounds on every field are intentionally conservative: they are not the
absolute service maxima but the values that produce a useful security
check. A user is free to keep the built-in default by omitting the key —
out-of-range values are dropped with a warning at SDK runtime, and
rejected at the Prowler App backend.

Whenever an upper bound is uncertain, the cap is set to a value that
still keeps the check meaningful (e.g. a 10-year window for date-based
thresholds) and avoids ints that obviously break downstream maths
(`min_kinesis_stream_retention_hours = 99999`).
"""

from typing import Annotated, Literal, Optional

from pydantic import AfterValidator, BeforeValidator, Field

from prowler.config.schema.base import ProviderConfigBase
from prowler.config.schema.validators import (
    make_dotted_version_validator,
    validate_ip_networks,
    validate_port_range,
)

# ---- Reusable constants -----------------------------------------------------

# CloudWatch Logs only accepts these retention values (in days). Anything else
# is silently coerced to the next valid value by the API — we reject upfront.
_CLOUDWATCH_RETENTION_DAYS = (
    1,
    3,
    5,
    7,
    14,
    30,
    60,
    90,
    120,
    150,
    180,
    365,
    400,
    545,
    731,
    1096,
    1827,
    2192,
    2557,
    2922,
    3288,
    3653,
)

_VALID_CW_RETENTION_LITERAL = Literal[
    1,
    3,
    5,
    7,
    14,
    30,
    60,
    90,
    120,
    150,
    180,
    365,
    400,
    545,
    731,
    1096,
    1827,
    2192,
    2557,
    2922,
    3288,
    3653,
]


# ---- Custom validators ------------------------------------------------------


# Reusable validators shared across providers (see schema/validators.py).
_validate_port_range = validate_port_range
_validate_trusted_ips = validate_ip_networks
# "1.4.0" style strings (used by Fargate platform versions).
_validate_semver = make_dotted_version_validator(3, 3)
# "1.28" style strings (EKS minor versions).
_validate_eks_minor = make_dotted_version_validator(2, 2)


def _validate_account_ids(v: Optional[list[str]]) -> Optional[list[str]]:
    if v is None:
        return v
    for account_id in v:
        if not (account_id.isdigit() and len(account_id) == 12):
            raise ValueError(
                f"trusted_account_ids entry {account_id!r} is not a 12-digit AWS account id"
            )
    return v


def _reject_bool_resource_limit(v):
    if isinstance(v, bool):
        raise ValueError("resource scan limits must be integers, not booleans")
    return v


ResourceScanLimit = Annotated[
    Optional[int], BeforeValidator(_reject_bool_resource_limit)
]


# ---- Main schema ------------------------------------------------------------


class AWSProviderConfig(ProviderConfigBase):
    # --- Resource scan limits ---------------------------------------------
    max_scanned_resources_per_service: ResourceScanLimit = Field(
        default=None,
        ge=-1,
        le=1_000_000,
        description="Global resource scan limit for high-volume AWS services. Use 0 or -1 to disable.",
    )
    max_ebs_snapshots: ResourceScanLimit = Field(
        default=None,
        ge=-1,
        le=1_000_000,
        description="Resource scan limit for EBS snapshots. Use 0 or -1 to disable.",
    )
    max_backup_recovery_points: ResourceScanLimit = Field(
        default=None,
        ge=-1,
        le=1_000_000,
        description="Resource scan limit for AWS Backup recovery points. Use 0 or -1 to disable.",
    )
    max_cloudwatch_log_groups: ResourceScanLimit = Field(
        default=None,
        ge=-1,
        le=1_000_000,
        description="Resource scan limit for CloudWatch log groups. Use 0 or -1 to disable.",
    )
    max_lambda_functions: ResourceScanLimit = Field(
        default=None,
        ge=-1,
        le=1_000_000,
        description="Resource scan limit for Lambda functions. Use 0 or -1 to disable.",
    )
    max_ecs_task_definitions: ResourceScanLimit = Field(
        default=None,
        ge=-1,
        le=1_000_000,
        description="Resource scan limit for ECS task definitions. Use 0 or -1 to disable.",
    )
    max_codeartifact_packages: ResourceScanLimit = Field(
        default=None,
        ge=-1,
        le=1_000_000,
        description="Resource scan limit for CodeArtifact packages. Use 0 or -1 to disable.",
    )

    # --- IAM ---------------------------------------------------------------
    mute_non_default_regions: Optional[bool] = None
    disallowed_regions: Optional[list[str]] = None
    max_unused_access_keys_days: Optional[int] = Field(
        default=None,
        ge=30,
        le=180,
        description=(
            "Days an IAM user access key can stay unused before being flagged. "
            "Range: 30..180 days (CIS AWS 1.13 recommends 45; NIST IA-5 ≤90)."
        ),
    )
    max_console_access_days: Optional[int] = Field(
        default=None,
        ge=30,
        le=180,
        description=(
            "Days an IAM console password can stay unused before being flagged. "
            "Range: 30..180 days (CIS AWS 1.12 recommends 45)."
        ),
    )
    max_unused_sagemaker_access_days: Optional[int] = Field(
        default=None,
        ge=7,
        le=180,
        description=(
            "Days a SageMaker user access key can stay unused. Range: 7..180 "
            "(SageMaker tokens are usually high-privilege over S3/KMS)."
        ),
    )

    # --- EC2 ---------------------------------------------------------------
    shodan_api_key: Optional[str] = Field(
        default=None,
        max_length=512,
        description="API key for Shodan lookups on EC2 public IPs.",
    )
    max_security_group_rules: Optional[int] = Field(
        default=None,
        ge=1,
        le=1000,
        description="Max ingress+egress rules per security group. AWS hard limit is 1000.",
    )
    max_ec2_instance_age_in_days: Optional[int] = Field(
        default=None,
        ge=1,
        le=1095,
        description=(
            "Days an EC2 instance can run before being flagged as old. "
            "Range: 1..1095 (3 years; instances should be refreshed for patching "
            "per NIST CM-3 — anything older is a security smell)."
        ),
    )
    ec2_allowed_interface_types: Optional[list[str]] = None
    ec2_allowed_instance_owners: Optional[list[str]] = None
    ec2_high_risk_ports: Annotated[
        Optional[list[int]], AfterValidator(_validate_port_range)
    ] = Field(
        default=None,
        description="TCP/UDP ports considered high-risk when reachable from the Internet (1..65535; port 0 is reserved).",
    )

    # --- ECS ---------------------------------------------------------------
    fargate_linux_latest_version: Annotated[
        Optional[str], AfterValidator(_validate_semver)
    ] = Field(default=None, description="Fargate Linux platform version (X.Y.Z).")
    fargate_windows_latest_version: Annotated[
        Optional[str], AfterValidator(_validate_semver)
    ] = Field(default=None, description="Fargate Windows platform version (X.Y.Z).")

    # --- Cross-account trust ----------------------------------------------
    trusted_account_ids: Annotated[
        Optional[list[str]], AfterValidator(_validate_account_ids)
    ] = Field(
        default=None,
        description="Additional 12-digit AWS account IDs trusted by cross-account checks.",
    )
    trusted_ips: Annotated[
        Optional[list[str]], AfterValidator(_validate_trusted_ips)
    ] = Field(
        default=None,
        description="IPv4/IPv6 addresses or CIDR ranges that are NOT considered public.",
    )

    # --- CloudWatch / CloudFormation --------------------------------------
    log_group_retention_days: Optional[_VALID_CW_RETENTION_LITERAL] = Field(
        default=None,
        description=(
            "Required CloudWatch Logs retention in days. Must match one of the "
            f"values accepted by the AWS API: {list(_CLOUDWATCH_RETENTION_DAYS)}."
        ),
    )
    recommended_cdk_bootstrap_version: Optional[int] = Field(
        default=None,
        ge=1,
        le=100,
        description="Min CDK bootstrap version expected on the account.",
    )

    # --- AppStream --------------------------------------------------------
    max_idle_disconnect_timeout_in_seconds: Optional[int] = Field(
        default=None,
        ge=60,
        le=1800,
        description=(
            "AppStream idle disconnect timeout (seconds). Range: 60..1800 "
            "(NIST AC-12: sensitive sessions ≤15 min — cap at 30 min)."
        ),
    )
    max_disconnect_timeout_in_seconds: Optional[int] = Field(
        default=None,
        ge=60,
        le=3600,
        description="AppStream disconnect timeout (seconds). Range: 60..3600.",
    )
    max_session_duration_seconds: Optional[int] = Field(
        default=None,
        ge=600,
        le=86400,
        description=(
            "AppStream max session duration (seconds). Range: 600..86400 "
            "(10 min .. 24 h — AWS AppStream hard limit per session)."
        ),
    )

    # --- Lambda -----------------------------------------------------------
    obsolete_lambda_runtimes: Optional[list[str]] = None
    lambda_min_azs: Optional[int] = Field(
        default=None,
        ge=1,
        le=6,
        description="Min number of AZs a VPC-bound Lambda must span. Range: 1..6.",
    )

    # --- Organizations ----------------------------------------------------
    organizations_enabled_regions: Optional[list[str]] = None
    organizations_trusted_delegated_administrators: Annotated[
        Optional[list[str]], AfterValidator(_validate_account_ids)
    ] = None
    organizations_trusted_ids: Optional[list[str]] = None

    # --- ECR --------------------------------------------------------------
    ecr_repository_vulnerability_minimum_severity: Optional[
        Literal["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFORMATIONAL"]
    ] = Field(
        default=None,
        description="Highest severity tolerated for ECR images.",
    )

    # --- Trusted Advisor --------------------------------------------------
    verify_premium_support_plans: Optional[bool] = None

    # --- CloudTrail threat detection: privilege escalation ----------------
    threat_detection_privilege_escalation_threshold: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Fraction of suspicious actions that triggers the priv-esc detection.",
    )
    threat_detection_privilege_escalation_minutes: Optional[int] = Field(
        default=None,
        ge=5,
        le=43200,
        description=(
            "Lookback window (minutes) for priv-esc detection. Range: 5..43200 "
            "(under 5 min the signal is dominated by false positives)."
        ),
    )
    threat_detection_privilege_escalation_actions: Optional[list[str]] = None

    # --- CloudTrail threat detection: enumeration -------------------------
    threat_detection_enumeration_threshold: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Fraction of suspicious actions that triggers the enumeration detection.",
    )
    threat_detection_enumeration_minutes: Optional[int] = Field(
        default=None,
        ge=5,
        le=43200,
        description="Lookback window (minutes) for enumeration detection. Range: 5..43200.",
    )
    threat_detection_enumeration_actions: Optional[list[str]] = None

    # --- CloudTrail threat detection: LLM jacking -------------------------
    threat_detection_llm_jacking_threshold: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Fraction of suspicious actions that triggers the LLM-jacking detection.",
    )
    threat_detection_llm_jacking_minutes: Optional[int] = Field(
        default=None,
        ge=5,
        le=43200,
        description="Lookback window (minutes) for LLM-jacking detection. Range: 5..43200.",
    )
    threat_detection_llm_jacking_actions: Optional[list[str]] = None

    # --- RDS --------------------------------------------------------------
    check_rds_instance_replicas: Optional[bool] = None

    # --- ACM --------------------------------------------------------------
    days_to_expire_threshold: Optional[int] = Field(
        default=None,
        ge=7,
        le=365,
        description=(
            "Days before certificate expiration to flag. Range: 7..365 "
            "(PCI-DSS 4.2.1.1: alert ≥30 days before expiry; <7 days is too "
            "tight to actually act on)."
        ),
    )
    insecure_key_algorithms: Optional[list[str]] = None

    # --- EKS --------------------------------------------------------------
    eks_required_log_types: Optional[
        list[
            Literal[
                "api",
                "audit",
                "authenticator",
                "controllerManager",
                "scheduler",
            ]
        ]
    ] = Field(
        default=None,
        description="EKS control plane log types that must be enabled.",
    )
    eks_cluster_oldest_version_supported: Annotated[
        Optional[str], AfterValidator(_validate_eks_minor)
    ] = Field(
        default=None,
        description='Minimum supported EKS minor version, expected as "X.Y".',
    )

    # --- CodeBuild --------------------------------------------------------
    excluded_sensitive_environment_variables: Optional[list[str]] = None
    codebuild_github_allowed_organizations: Optional[list[str]] = None

    # --- ELB / ELBv2 ------------------------------------------------------
    elb_min_azs: Optional[int] = Field(
        default=None,
        ge=1,
        le=6,
        description="Min AZs a Classic ELB must span. Range: 1..6.",
    )
    elbv2_min_azs: Optional[int] = Field(
        default=None,
        ge=1,
        le=6,
        description="Min AZs an Application/Network LB must span. Range: 1..6.",
    )
    elbv2_listener_pqc_tls_allowed_policies: Optional[list[str]] = Field(
        default=None,
        description="ELBv2 SSL policies that satisfy the PQ TLS listener check.",
    )

    # --- ElastiCache -----------------------------------------------------
    minimum_snapshot_retention_period: Optional[int] = Field(
        default=None,
        ge=1,
        le=35,
        description="Days an ElastiCache backup must be retained. Range: 1..35 (service hard limit).",
    )

    # --- Secrets ---------------------------------------------------------
    secrets_ignore_patterns: Optional[list[str]] = None
    secrets_ignore_files: Optional[list[str]] = None
    secrets_validate: Optional[bool] = Field(
        default=None,
        description=(
            "Validate discovered secrets against the provider APIs (live check). "
            "Makes outbound network calls that authenticate with the discovered "
            "secret. Disabled by default."
        ),
    )
    max_days_secret_unused: Optional[int] = Field(
        default=None,
        ge=7,
        le=365,
        description="Days a Secrets Manager secret can stay unused. Range: 7..365.",
    )
    max_days_secret_unrotated: Optional[int] = Field(
        default=None,
        ge=1,
        le=180,
        description=(
            "Days a Secrets Manager secret can go without rotation. Range: 1..180 "
            "(NIST IA-5: rotate quarterly; CIS recommends ≤90)."
        ),
    )

    # --- Kinesis ---------------------------------------------------------
    min_kinesis_stream_retention_hours: Optional[int] = Field(
        default=None,
        ge=24,
        le=8760,
        description="Hours of Kinesis stream retention. Range: 24..8760 (1 day .. 1 year).",
    )

    # --- S3 --------------------------------------------------------------
    s3_bucket_object_public_enabled: Optional[bool] = Field(
        default=None,
        description=(
            "Enable the s3_bucket_object_public spot-check, which samples object "
            "ACLs per bucket. Disabled by default because it lists and reads object "
            "ACLs, which is expensive on large buckets."
        ),
    )
    s3_bucket_object_public_max_objects: Optional[int] = Field(
        default=None,
        ge=1,
        le=1000,
        description=(
            "Max objects to list per bucket as the sampling pool. Range: 1..1000 "
            "(ListObjectsV2 returns at most 1000 keys per page)."
        ),
    )
    s3_bucket_object_public_sample_size: Optional[int] = Field(
        default=None,
        ge=1,
        le=1000,
        description=(
            "Number of objects sampled from the listed pool for ACL inspection. "
            "Range: 1..1000. Must be positive to avoid a no-op or invalid sample."
        ),
    )
