package com.cloudforgeci.api.ingress;

import com.cloudforgeci.api.core.annotation.BaseFactory;
import com.cloudforgeci.api.core.rules.AwsConfigRule;
import com.cloudforge.core.annotation.DeploymentContext;
import com.cloudforge.core.annotation.SystemContext;
import com.cloudforge.core.enums.RuntimeType;
import software.amazon.awscdk.*;
import software.amazon.awscdk.customresources.AwsCustomResource;
import software.amazon.awscdk.customresources.AwsCustomResourcePolicy;
import software.amazon.awscdk.customresources.AwsSdkCall;
import software.amazon.awscdk.customresources.PhysicalResourceId;
import software.amazon.awscdk.services.ec2.Peer;
import software.amazon.awscdk.services.ec2.Port;
import software.amazon.awscdk.services.ec2.SecurityGroup;
import software.amazon.awscdk.services.ec2.Vpc;
import software.amazon.awscdk.services.elasticloadbalancingv2.*;
import software.amazon.awscdk.services.iam.AnyPrincipal;
import software.amazon.awscdk.services.iam.Effect;
import software.amazon.awscdk.services.iam.PolicyStatement;
import software.amazon.awscdk.services.s3.*;
import software.constructs.Construct;
import io.github.cdklabs.cdknag.NagSuppressions;
import io.github.cdklabs.cdknag.NagPackSuppression;

import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

/**
 * ALB Factory using annotation-based context injection.
 * This demonstrates the cleaner approach without passing SystemContext as parameters.
 */
public class AlbFactory extends BaseFactory {

    private static final Logger LOG = Logger.getLogger(AlbFactory.class.getName());

    @SystemContext("runtime")
    private RuntimeType runtime;

    @SystemContext("vpc")
    private Vpc vpc;

    @DeploymentContext("healthCheckInterval")
    private Integer healthCheckInterval;

    @DeploymentContext("healthCheckTimeout")
    private Integer healthCheckTimeout;

    @DeploymentContext("healthyThreshold")
    private Integer healthyThreshold;

    @DeploymentContext("unhealthyThreshold")
    private Integer unhealthyThreshold;

    @DeploymentContext("enableSsl")
    private Boolean enableSsl;

    @DeploymentContext("albAccessLogging")
    private Boolean albAccessLogging;

    @DeploymentContext("region")
    private String region;

    @DeploymentContext("stackName")
    private String stackName;

    @SystemContext("securityProfileConfig")
    private com.cloudforgeci.api.interfaces.SecurityProfileConfiguration securityProfileConfig;

    @SystemContext("applicationSpec")
    private com.cloudforge.core.interfaces.ApplicationSpec applicationSpec;

    public AlbFactory(Construct scope, String id) {
        super(scope, id);
    }

    @Override
    public void create() {
        if (ctx == null) {
            throw new IllegalStateException("SystemContext is null in AlbFactory.create()");
        }

        try {
            // Get compliance settings from security profile (injected via annotation)
            if (securityProfileConfig != null && albAccessLogging == null) {
                // Use security profile setting if not explicitly configured in deployment context
                albAccessLogging = securityProfileConfig.isAlbAccessLoggingEnabled();
                LOG.info("ALB access logging inherited from security profile: " + albAccessLogging);
            }

            // Create security group
            SecurityGroup albSg = createSecurityGroup();
            ctx.albSg.set(albSg);

            // Create ALB
            ApplicationLoadBalancer alb = createLoadBalancer(albSg);
            ctx.alb.set(alb);

            // Check if HTTPS strict mode is enabled (skip HTTP listener entirely for compliance)
            boolean httpsStrict = config != null && config.isHttpsStrictEnabled();
            boolean sslEnabled = Boolean.TRUE.equals(enableSsl);

            if (httpsStrict && sslEnabled) {
                // HTTPS strict mode: No HTTP listener for PCI-DSS compliance (Req 4.1)
                // Target group wiring will be handled by RuntimeConfiguration after HTTPS listener is created
                LOG.info("HTTPS strict mode enabled: Skipping HTTP listener (port 80) for compliance");
                LOG.info("  Target group wiring will be handled by RuntimeConfiguration");
                // ctx.http remains empty - HTTPS listener will be created by RuntimeConfiguration
            } else {
                // Create HTTP listener with placeholder default action
                // The target group will be created by orchestration layer and added to listener later
                ApplicationListener http = createFargateHttpListener(alb, sslEnabled);
                ctx.http.set(http);
            }

        } catch (Exception e) {
            LOG.severe("Exception in AlbFactory.create(): " + e.getMessage());
            throw e;
        }
    }

    private SecurityGroup createSecurityGroup() {
        // Check if egress should be restricted to VPC CIDR only
        boolean restrictEgress = config.isRestrictSecurityGroupEgressEnabled();

        SecurityGroup sg = SecurityGroup.Builder.create(this, "AlbSg")
                .vpc(vpc)
                .description("ALB Security Group")
                .allowAllOutbound(!restrictEgress)
                .build();

        // If egress is restricted, add explicit egress rule for VPC CIDR only
        // ALB only needs to communicate with backend targets within VPC
        if (restrictEgress) {
            sg.addEgressRule(
                Peer.ipv4(vpc.getVpcCidrBlock()),
                Port.allTraffic(),
                "Allow egress to VPC CIDR only (backend targets)"
            );
        }

        // Suppress EC23 for ALB security group - public-facing load balancer requires open ingress
        NagSuppressions.addResourceSuppressions(
            sg,
            List.of(
                NagPackSuppression.builder()
                    .id("AwsSolutions-EC23")
                    .reason("ALB security group requires open ingress on ports 80/443 for public-facing " +
                            "load balancer. This is the intended architecture for serving web applications " +
                            "to internet users. Backend instances are protected in private subnets.")
                    .build()
            ),
            Boolean.TRUE
        );

        return sg;
    }

    private ApplicationLoadBalancer createLoadBalancer(SecurityGroup albSg) {
        // Enable access logging for compliance if configured
        if (Boolean.TRUE.equals(albAccessLogging)) {
            // Get region from deployment context
            final String effectiveRegion = (region != null && !region.isEmpty()) ? region : "us-east-1";

            // Validate required fields for ALB access logging
            String validationError = validateLoggingPrerequisites(effectiveRegion, stackName);
            if (validationError != null) {
                LOG.warning("ALB access logging enabled but prerequisites not met: " + validationError);
                return createAlbWithoutLogging(albSg);
            }

            // Get stack account for bucket naming (captured in lambda)
            final String accountId = Stack.of(this).getAccount();

            // Use Lazy.uncachedString() to defer bucket name construction until synthesis time
            String bucketName = Lazy.uncachedString(
                    new IStringProducer() {
                        @Override
                        public String produce(IResolveContext context) {
                            // STACK-SPECIFIC bucket name to avoid conflicts between stacks
                            return (stackName + "-alb-logs-" + accountId + "-" + effectiveRegion).toLowerCase();
                        }
                    },
                    LazyStringValueOptions.builder()
                            .displayHint(stackName + "-alb-logs-bucket")
                            .build()
            );

            LOG.info("ALB logs bucket name (stack-specific): " + bucketName);

            // Determine removal policy based on security profile (injected via annotation)
            boolean isProduction = (securityProfileConfig != null && securityProfileConfig.getClass().getSimpleName().contains("Production"));
            RemovalPolicy removalPolicy = isProduction ?
                    RemovalPolicy.RETAIN :
                    RemovalPolicy.DESTROY;

            // Get or create ALB logs bucket with SSM tracking (PRODUCTION only)
            IBucket logBucket = getOrCreateAlbLogsBucketWithSSM(
                bucketName,
                removalPolicy,
                isProduction,
                effectiveRegion
            );

            String appId = applicationSpec != null ? applicationSpec.applicationId() : "app";
            String albId = Character.toUpperCase(appId.charAt(0)) + appId.substring(1) + "Alb";

            ApplicationLoadBalancer alb = ApplicationLoadBalancer.Builder.create(this, albId)
                    .vpc(vpc)
                    .securityGroup(albSg)
                    .internetFacing(true)
                    .deletionProtection(shouldEnableDeletionProtection())
                    .build();

            // Enable security compliance settings
            configureAlbSecurity(alb);

            // Enable access logs on the created ALB
            alb.logAccessLogs(logBucket);

            // Note: SSL enforcement policy is added in getOrCreateAlbLogsBucketWithSSM but
            // CDK's logAccessLogs() creates a bucket policy that doesn't merge with addToResourcePolicy()
            // statements. The ALB access logs bucket is only written by AWS ELB service via internal
            // HTTPS connections, and has blockPublicAccess enabled. See PCI-DSS suppression in InteractiveDeployer.

            // Register AWS Config rules for ALB access logging compliance
            ctx.requireConfigRule(AwsConfigRule.ELB_LOGGING_ENABLED);

            LOG.info("ALB access logging enabled");
            LOG.info("  S3 Bucket: " + logBucket.getBucketName());
            LOG.info("  Retention: 6 years (2190 days)");
            LOG.info("  Lifecycle: Glacier (90d), Deep Archive (1y), Delete (6y)");
            LOG.info("  Encryption: S3-managed (SSE-S3)");
            LOG.info("  Drop invalid HTTP headers: enabled (compliance)");

            return alb;
        } else {
            LOG.info("ALB access logging is disabled");
            LOG.info("  Note: Access logs may be required for audit and compliance frameworks");
            LOG.info("  Enable by setting albAccessLogging = true or using PRODUCTION/STAGING security profile");

            return createAlbWithoutLogging(albSg);
        }
    }

    private ApplicationLoadBalancer createAlbWithoutLogging(SecurityGroup albSg) {
        String appId = applicationSpec != null ? applicationSpec.applicationId() : "app";
        String albId = Character.toUpperCase(appId.charAt(0)) + appId.substring(1) + "Alb";

        ApplicationLoadBalancer alb = ApplicationLoadBalancer.Builder.create(this, albId)
                .vpc(vpc)
                .securityGroup(albSg)
                .internetFacing(true)
                .deletionProtection(shouldEnableDeletionProtection())
                .build();

        // Enable security compliance settings
        configureAlbSecurity(alb);

        // Add suppression for ELB2 when access logs are disabled (DEV profile cost optimization)
        NagSuppressions.addResourceSuppressions(
            alb,
            List.of(
                NagPackSuppression.builder()
                    .id("AwsSolutions-ELB2")
                    .reason("ALB access logging is intentionally disabled for DEV/non-production " +
                            "environments to reduce S3 storage costs. Production deployments enable " +
                            "access logging for audit compliance and security monitoring.")
                    .build()
            ),
            Boolean.TRUE
        );

        return alb;
    }

    /**
     * Determine if deletion protection should be enabled based on security profile.
     * Production security profiles enable deletion protection by default.
     */
    private boolean shouldEnableDeletionProtection() {
        if (securityProfileConfig == null) {
            return false; // Default to disabled if no security profile
        }

        // Check if this is a production-grade security profile
        boolean isProduction = securityProfileConfig.getClass().getSimpleName().contains("Production");

        if (isProduction) {
            // Register AWS Config rule for deletion protection compliance
            ctx.requireConfigRule(AwsConfigRule.ELB_DELETION_PROTECTION);
            LOG.info("ALB deletion protection: ENABLED (Production security profile)");
            return true;
        } else {
            LOG.info("ALB deletion protection: DISABLED (Dev/Staging security profile)");
            return false;
        }
    }

    /**
     * Configure ALB security settings for compliance (SOC2, PCI-DSS).
     * Enables dropping invalid HTTP headers to prevent header injection attacks.
     */
    private void configureAlbSecurity(ApplicationLoadBalancer alb) {
        CfnLoadBalancer cfnAlb = (CfnLoadBalancer) alb.getNode().getDefaultChild();
        cfnAlb.addPropertyOverride("LoadBalancerAttributes", List.of(
            Map.of("Key", "routing.http.drop_invalid_header_fields.enabled", "Value", "true")
        ));
        LOG.info("Drop invalid HTTP headers: enabled (compliance)");
    }

    /**
     * Validate prerequisites for ALB access logging.
     *
     * @param region The AWS region (must not be null, empty, or contain CDK tokens)
     * @param stackName The stack name (must not be null or empty)
     * @return Error message if validation fails, null if validation passes
     */
    /**
     * Get or create ALB logs bucket with SSM tracking for PRODUCTION mode.
     *
     * For PRODUCTION mode:
     * - Create bucket WITHOUT explicit name (CloudFormation generates unique name)
     * - Store ARN in SSM at deployment time for tracking
     * - This prevents conflicts with retained buckets
     *
     * For DEV/STAGING mode:
     * - Create bucket with explicit name (will be destroyed with stack)
     */
    private IBucket getOrCreateAlbLogsBucketWithSSM(String bucketName, RemovalPolicy removalPolicy,
                                                     boolean isProduction, String region) {
        if (!isProduction) {
            // DEV/STAGING: Create bucket without SSM tracking (will be deleted with stack)
            // autoDeleteObjects=true ensures bucket contents are emptied before deletion
            LOG.info("Non-production mode: Creating ALB logs bucket without SSM tracking (auto-delete enabled)");
            return createAlbLogsBucket(bucketName, removalPolicy, true);
        }

        // PRODUCTION: Create bucket WITHOUT explicit name and track ARN in SSM
        LOG.info("Production mode: Creating ALB logs bucket with auto-generated name");
        LOG.info("  CloudFormation will generate unique bucket name to avoid conflicts");

        // Create bucket WITHOUT specifying bucketName - CloudFormation generates unique name
        Bucket newBucket = Bucket.Builder.create(this, "AlbLogsBucket")
                // NO bucketName specified - CloudFormation auto-generates unique name
                .encryption(BucketEncryption.S3_MANAGED)
                .blockPublicAccess(BlockPublicAccess.BLOCK_ALL)
                .removalPolicy(removalPolicy)
                .autoDeleteObjects(false)  // Never auto-delete in PRODUCTION
                .versioned(true)
                .lifecycleRules(List.of(
                    LifecycleRule.builder()
                        .transitions(List.of(
                            Transition.builder()
                                .storageClass(StorageClass.GLACIER)
                                .transitionAfter(Duration.days(90))
                                .build(),
                            Transition.builder()
                                .storageClass(StorageClass.DEEP_ARCHIVE)
                                .transitionAfter(Duration.days(365))
                                .build()
                        ))
                        .expiration(Duration.days(2190))
                        .build()
                ))
                .build();

        // Enforce SSL for all S3 requests (PCI-DSS Req 4.1)
        newBucket.addToResourcePolicy(PolicyStatement.Builder.create()
                .sid("DenyInsecureTransport")
                .effect(Effect.DENY)
                .principals(List.of(new AnyPrincipal()))
                .actions(List.of("s3:*"))
                .resources(List.of(
                        newBucket.getBucketArn(),
                        newBucket.arnForObjects("*")
                ))
                .conditions(java.util.Map.of(
                        "Bool", java.util.Map.of("aws:SecureTransport", "false")
                ))
                .build());
        // Note: This SSL policy is added but may not appear in synthesized CloudFormation
        // due to CDK limitation where logAccessLogs() bucket policy doesn't merge with
        // addToResourcePolicy() statements. See PCI-DSS suppression in InteractiveDeployer.

        // Add NagSuppressions for CDK limitations and justified architecture decisions
        NagSuppressions.addResourceSuppressions(
            newBucket,
            List.of(
                NagPackSuppression.builder()
                    .id("AwsSolutions-S1")
                    .reason("ALB access logs bucket receives logs from ALB. Server access logging " +
                           "would create circular dependency. CloudTrail S3 data events provide audit logging.")
                    .build(),
                NagPackSuppression.builder()
                    .id("PCI.DSS.321-S3BucketReplicationEnabled")
                    .reason("S3 replication is not required for single-region deployments. " +
                           "ALB access logs are retained with versioning enabled for compliance.")
                    .build(),
                NagPackSuppression.builder()
                    .id("PCI.DSS.321-S3BucketLoggingEnabled")
                    .reason("ALB access logs bucket receives logs from ALB. Server access logging " +
                           "would create circular dependency. CloudTrail S3 data events provide audit logging.")
                    .build(),
                NagPackSuppression.builder()
                    .id("PCI.DSS.321-S3DefaultEncryptionKMS")
                    .reason("ALB access logging requires S3-managed encryption. KMS encryption is not " +
                           "supported for ALB access logs due to AWS service limitations.")
                    .build()
            ),
            Boolean.TRUE
        );

        // Store bucket ARN in SSM at deployment time using Custom Resource (stack-scoped)
        String ssmParameterName = "/cloudforge/shared/" + region + "/stack/" + this.stackName + "/alb-logs/bucket-arn";

        AwsSdkCall putParameterCall = AwsSdkCall.builder()
                .service("SSM")
                .action("putParameter")
                .parameters(java.util.Map.of(
                        "Name", ssmParameterName,
                        "Value", newBucket.getBucketArn(),
                        "Type", "String",
                        "Description", "CloudForge retained ALB logs bucket ARN for region " + region,
                        "Overwrite", true
                ))
                .physicalResourceId(PhysicalResourceId.of("AlbLogsBucket-SSMWriter"))
                .region(region)
                .build();

        AwsCustomResource ssmWriter = AwsCustomResource.Builder.create(this, "AlbLogsBucketSSMWriter")
                .onCreate(putParameterCall)
                .onUpdate(putParameterCall)
                .policy(AwsCustomResourcePolicy.fromSdkCalls(
                        software.amazon.awscdk.customresources.SdkCallsPolicyOptions.builder()
                                .resources(List.of("*"))
                                .build()
                ))
                .build();

        // Add NagSuppressions for CDK custom resource limitations
        NagSuppressions.addResourceSuppressions(
            ssmWriter,
            List.of(
                NagPackSuppression.builder()
                    .id("PCI.DSS.321-IAMNoInlinePolicy")
                    .reason("CDK AwsCustomResource creates inline policies by design. " +
                           "These are auto-generated Lambda execution policies for AWS SDK calls.")
                    .build(),
                NagPackSuppression.builder()
                    .id("PCI.DSS.321-LambdaInsideVPC")
                    .reason("CDK custom resource Lambdas only make AWS API calls (SSM) " +
                           "and do not require VPC access.")
                    .build(),
                NagPackSuppression.builder()
                    .id("AwsSolutions-IAM5")
                    .reason("SSM parameter operations require wildcard resource patterns.")
                    .build()
            ),
            Boolean.TRUE
        );

        ssmWriter.getNode().addDependency(newBucket);

        LOG.info("ALB logs bucket will use CloudFormation-generated unique name");
        LOG.info("ALB logs bucket ARN will be stored in SSM: " + ssmParameterName);
        return newBucket;
    }

    /**
     * Create ALB logs S3 bucket with compliance-driven lifecycle rules.
     */
    private Bucket createAlbLogsBucket(String bucketName, RemovalPolicy removalPolicy, boolean autoDelete) {
        Bucket bucket = Bucket.Builder.create(this, "AlbLogsBucket")
                .bucketName(bucketName)
                .encryption(BucketEncryption.S3_MANAGED)
                .blockPublicAccess(BlockPublicAccess.BLOCK_ALL)
                .removalPolicy(removalPolicy)
                .autoDeleteObjects(autoDelete)
                .versioned(true)  // Enable versioning for compliance (SOC2/PCI-DSS/HIPAA)
                .lifecycleRules(List.of(
                    LifecycleRule.builder()
                        .transitions(List.of(
                            Transition.builder()
                                .storageClass(StorageClass.GLACIER)
                                .transitionAfter(Duration.days(90))
                                .build(),
                            Transition.builder()
                                .storageClass(StorageClass.DEEP_ARCHIVE)
                                .transitionAfter(Duration.days(365))
                                .build()
                        ))
                        .expiration(Duration.days(2190))
                        .build()
                ))
                .build();

        // Enforce SSL for all S3 requests (PCI-DSS Req 4.1)
        bucket.addToResourcePolicy(PolicyStatement.Builder.create()
                .sid("DenyInsecureTransport")
                .effect(Effect.DENY)
                .principals(List.of(new AnyPrincipal()))
                .actions(List.of("s3:*"))
                .resources(List.of(
                        bucket.getBucketArn(),
                        bucket.arnForObjects("*")
                ))
                .conditions(java.util.Map.of(
                        "Bool", java.util.Map.of("aws:SecureTransport", "false")
                ))
                .build());
        LOG.info("  SSL enforcement policy added to bucket");

        return bucket;
    }

    private String validateLoggingPrerequisites(String region, String stackName) {
        if (region == null || region.isEmpty() || region.contains("$")) {
            return "Region is not available. Set 'region' in deployment context or CDK_DEFAULT_REGION environment variable";
        }
        if (stackName == null || stackName.isEmpty()) {
            return "Stack name is not set. Set 'stackName' in deployment context to enable ALB access logging";
        }
        return null; // Validation passed
    }

    private ApplicationTargetGroup createTargetGroup(ApplicationLoadBalancer alb) {
        // Use configurable health check settings from annotated fields
        int interval = healthCheckInterval != null ? healthCheckInterval : 30;
        int timeout = healthCheckTimeout != null ? healthCheckTimeout : 5;
        int healthy = healthyThreshold != null ? healthyThreshold : 2;
        int unhealthy = unhealthyThreshold != null ? unhealthyThreshold : 3;

        // Get application-specific health check path and port
        String healthCheckPath = applicationSpec != null ? applicationSpec.healthCheckPath() : "/";
        int applicationPort = applicationSpec != null ? applicationSpec.applicationPort() : 8080;

        String appId = applicationSpec != null ? applicationSpec.applicationId() : "app";
        String tgId = Character.toUpperCase(appId.charAt(0)) + appId.substring(1) + "Tg";

        return ApplicationTargetGroup.Builder.create(this, tgId)
                .vpc(vpc)
                .port(applicationPort)
                .protocol(ApplicationProtocol.HTTP)
                .targetType(TargetType.INSTANCE)
                .healthCheck(HealthCheck.builder()
                        .path(healthCheckPath)
                        .healthyHttpCodes("200-299")
                        .interval(Duration.seconds(interval))
                        .timeout(Duration.seconds(timeout))
                        .healthyThresholdCount(healthy)
                        .unhealthyThresholdCount(unhealthy)
                        .build())
                .build();
    }

    private ApplicationListener createHttpListener(ApplicationLoadBalancer alb, ApplicationTargetGroup targetGroup) {
        return alb.addListener("Http", BaseApplicationListenerProps.builder()
                .port(80)
                .defaultAction(ListenerAction.forward(List.of(targetGroup)))
                .build());
    }

    private ApplicationListener createFargateHttpListener(ApplicationLoadBalancer alb, boolean sslEnabled) {
        String appId = applicationSpec != null ? applicationSpec.applicationId() : "Application";
        String startupMessage = Character.toUpperCase(appId.charAt(0)) + appId.substring(1) + " is starting up...";

        // Create HTTP listener with a temporary default action
        // This will be updated by FargateRuntimeConfiguration when the Fargate service is ready
        return alb.addListener("Http", BaseApplicationListenerProps.builder()
                .port(80)
                .defaultAction(ListenerAction.fixedResponse(200, FixedResponseOptions.builder()
                        .contentType("text/plain")
                        .messageBody(startupMessage)
                        .build()))
                .build());
    }

    private ApplicationListener createHttpListenerWithoutTargetGroup(ApplicationLoadBalancer alb, boolean sslEnabled) {
        String appId = applicationSpec != null ? applicationSpec.applicationId() : "Application";
        String startupMessage = Character.toUpperCase(appId.charAt(0)) + appId.substring(1) + " is starting up...";

        // HTTP listener configuration is now handled by SecurityProfile wiring
        // SSL redirect logic is centralized in SecurityProfile.wire() method
        return alb.addListener("Http", BaseApplicationListenerProps.builder()
                .port(80)
                .defaultAction(ListenerAction.fixedResponse(200, FixedResponseOptions.builder()
                        .contentType("text/plain")
                        .messageBody(startupMessage)
                        .build()))
                .build());
    }
}
