<?php

/*
 * Creates AWS resources for Webien setup
 * Usage:
 * wp aws-setup create bucketname.se
 * wp aws-setup activate bucketname.se
 * append command with --silent for no output
 *
 * For new setup AWS storage won't be used
*/

use Aws\S3\S3Client;
use Aws\Exception\AwsException;
use Aws\CloudFront\CloudFrontClient;
use Aws\Iam\IamClient;
use Aws\Ses\SesClient;

class AwsSetup extends WP_CLI_Command {

    private $silent;
    private $sdk;
    private $s3Client;
    private $cloudfrontClient;
    private $iamClient;
    private $distributionId = 'E31OF5O0UQCS34'; // Cloudfront main distribution id, this needs to be updated after every 250 buckets..
    private $originAccessIdentity = 'E2SHI0J0HDJINF'; // Shared Origin Access identity on Cloudfront -> https://console.aws.amazon.com/cloudfront/home?region=eu-north-1#oai:
    private $distributionUrl = 'c1.webien.cloud'; // Alias domain for cloudfront distribution. Also needs to be updated after every 250 buckets..

    private $sitename;
    private $rootDir;

    public $bucket;
    public $iamKey;
    public $iamSecret;


    function create($sitename, $assoc_args) {

        // Check if we should display output
        $arguments = wp_parse_args(
            $assoc_args, ['silent' => false]
        );
        if ( isset($arguments['silent']) ) {
            $this->silent = $arguments['silent'];
        }

        $this->sitename = $sitename[0];

        // Check if credentials are already setup in .env
        if(defined('AWS_BUCKET')){
            WP_CLI::warning( "Bucket credentials are already setup in .env file." );
            die();
        }

        // Standard settings
        $sharedConfig = [
            'region' => 'eu-north-1',
            'version' => 'latest'
        ];
        // Create an SDK class used to share configuration across clients.
        $this->sdk = new \Aws\Sdk($sharedConfig);
        // S3Client object.
        $this->s3Client = $this->sdk->createS3();
        // Cloudfront client
        $this->cloudfrontClient = new \Aws\CloudFront\CloudFrontClient($sharedConfig);
        // IAM client
        $this->iamClient = new IamClient($sharedConfig);

        // Name bucket with prefix
        $this->bucket = 'webien-'.$this->sitename;

        // Lets go
        $this->createBucket();
        $this->createBucketFolders();
        $this->setBucketPermissions();
        $this->createUser();
        $this->updateCloudfront();
        $this->updateEnv();
        //$this->deactivateS3plugin();
        //$this->addActivationCron();

        $this->silent ?: WP_CLI::success('Setup complete - needs to be activated once distribution is ready..');


    }


    private function createBucket(){
        // Create bucket
        // https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/s3-examples-creating-buckets.html
        try {
            $result = $this->s3Client->createBucket([
                'Bucket'=>$this->bucket,
                'ObjectOwnership' => 'ObjectWriter',
            ]);
            //var_dump($result);
            $this->silent ?: WP_CLI::log( 'Bucket created..' );
        } catch (AwsException $e) {
            echo $e->getMessage();
            echo "\n";
            WP_CLI::error( 'Bucket could not be created. Script stopped.' );
            die();
        }

        if ($result['@metadata']['statusCode'] == 200) {
            return true;
        } else {
            WP_CLI::error( 'Bucket could not be created for some reason. Script stopped.' );
            die();
        }

    }


    private function createBucketFolders(){
        // Create folder (needed to handle cloudfront rewrites)
        try {
            $result = $this->s3Client->putObject([
                'Bucket' => $this->bucket,
                'Key' => $this->sitename . '/',
            ]);
            $this->silent ?: WP_CLI::log( 'Bucket folders created..' );
        } catch (AwsException $e) {
            echo $e->getMessage();
            echo "\n";
            WP_CLI::error( 'Bucket folders could not be created. Script stopped.' );
            die();
        }

        // Copy index file from main bucket
        try {
            $result = $this->s3Client->copyObject([
                'Bucket'     => $this->bucket,
                'Key'        => $this->sitename . "/index.html",
                'CopySource' => "webien/index.html",
            ]);
            $this->silent ?: WP_CLI::log( 'Default files copied to new bucket folder..' );
        } catch (AwsException $e) {
            echo $e->getMessage();
            echo "\n";
            WP_CLI::error( 'Default files could not be copied. Script stopped.' );
            return false;
        }

    }

    private function setBucketPermissions(){
        // Set bucket policy to public
        // https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/s3-examples-bucket-policies.html
        try {

            // Remove default public access block
            // https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-s3-2006-03-01.html#putpublicaccessblock
            $result = $this->s3Client->putPublicAccessBlock([
                'Bucket' => $this->bucket,
                'PublicAccessBlockConfiguration' => [
                    'BlockPublicAcls' =>  false,
                    'BlockPublicPolicy' => false,
                    'IgnorePublicAcls' => false,
                    'RestrictPublicBuckets' => false,
                ],
            ]);

            // Add some policies to make it public
            $result = $this->s3Client->putBucketPolicy([
                'Bucket' => $this->bucket,
                'Policy' => '{
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Sid": "AddPerm",
                            "Effect": "Allow",
                            "Principal": "*",
                            "Action": "s3:GetObject",
                            "Resource": "arn:aws:s3:::' . $this->bucket . '/*"
                        },
                        {
                            "Sid": "2",
                            "Effect": "Allow",
                            "Principal": {
                                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E2SHI0J0HDJINF"
                            },
                            "Action": "s3:GetObject",
                            "Resource": "arn:aws:s3:::' . $this->bucket . '/*"
                        }
                    ]
                }'
            ]);
            $this->silent ?: WP_CLI::log( 'Bucket permissions set..' );
        } catch (AwsException $e) {
            // Display error message
            echo $e->getMessage();
            echo "\n";
            WP_CLI::error( 'Bucket permissions could not be set. Script stopped.' );
            die();
        }

        // Also add some cors to allow cross domain loading
        try {
            $result = $this->s3Client->putBucketCors([
                'Bucket' => $this->bucket,
                'CORSConfiguration' => [
                    'CORSRules' => [
                        [
                            'AllowedHeaders' => ['Authorization'],
                            'AllowedMethods' => ['GET', 'HEAD'],
                            'AllowedOrigins' => ['*'],
                            'ExposeHeaders' => [],
                            'MaxAgeSeconds' => 3000
                        ],
                    ],
                ]
            ]);
            $this->silent ?: WP_CLI::log( 'Bucket CORS set..' );
        } catch (AwsException $e) {
            echo($e->getMessage());
            echo "\n";
            WP_CLI::error( 'Bucket CORS could not be set. Script stopped.' );
            die();
        }

        // Enable versioning
        try {
            $result = $this->s3Client->putBucketVersioning([
                'Bucket' => $this->bucket,
                'VersioningConfiguration' => [
                    'MFADelete' => 'Disabled',
                    'Status' => 'Enabled',
                ],
            ]);
            $this->silent ?: WP_CLI::log( 'Bucket versioning enabled..' );
            $this->silent ?: WP_CLI::log( 'Bucket complete..' );
        } catch (AwsException $e) {
            echo($e->getMessage());
            echo "\n";
            WP_CLI::error( 'Bucket CORS could not be set. Script stopped.' );
            die();
        }


    }


    private function createUser(){
        // Create iAm User and receive credentials valid only for this installation.
        // Inspired from s3 plugin
        // https://github.com/humanmade/S3-Uploads/blob/master/inc/class-s3-uploads-wp-cli-command.php#L78

        try {
            $this->iamClient->createUser([
                'UserName' => $this->sitename
            ]);

            $credentials = $this->iamClient->createAccessKey(['UserName' => $this->sitename]);
            $this->iamKey = $credentials['AccessKey']['AccessKeyId'];
            $this->iamSecret= $credentials['AccessKey']['SecretAccessKey'];

            $this->iamClient->putUserPolicy( array(
                'UserName'       => $this->sitename,
                'PolicyName'     => $this->sitename . '-policy',
                'PolicyDocument' => $this->getIamPolicy(),
            ));

            $this->silent ?: WP_CLI::log( 'User created..' );
        }


        catch (AwsException $e) {
            echo $e->getMessage();
            echo "\n";
            WP_CLI::error( 'User could not be created. Script stopped.' );
            die();
        }

    }


    private function updateCloudfront(){
        // Get current Cloudfront settings and update them for created bucket
        // https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/cloudfront-example-distribution.html

        // Get current settings
        try {
            $result = $this->cloudfrontClient->getDistribution([
                'Id' => $this->distributionId,
            ]);
        } catch (AwsException $e) {
            // output error message if fails
            echo $e->getMessage();
            echo "\n";
            WP_CLI::error( 'Could not find distribution. Script stopped.' );
            die();
        }


        // First check that we haven't surpassed 250 origins, since that's maximum a single distribution can handle on the webien aws account
        $originCount = count($result['Distribution']['DistributionConfig']['Origins']['Items']);
        if($originCount > 230){
            $this->sendAdminMail('Antal buckets som kan distribueras på aktuell Cloudfront distribution håller på att ta slut. Antal nu är: <b>'.$originCount.'/250</b>.<br>Förbered genom att skapa en ny cloudfront distribution och uppdatera ID:n i webien pluggen.');
            $this->silent ?: WP_CLI::warning( 'Antal buckets som kan distribueras på aktuell Cloudfront distribution håller på att ta slut. Antal nu är:' . $originCount.'/250. Förbered genom att skapa en ny cloudfront distribution och uppdatera ID:n i webien pluggen.' );
        }
        if($originCount > 249){
            $this->sendAdminMail('Antal buckets på aktuell distribution har tagit slut. Skapa en ny cloudfront distribution och uppdatera ID:n i webien pluggen.. AWS uppsättning av: '.$this->sitename.' misslyckades..!');
            WP_CLI::error('Antal origins på denna distribution är slut, kontakta admin om att skapa ny distribution..');
            die();
        }

        // Store settings
        $currentConfig = $result["Distribution"]["DistributionConfig"];
        $ETag = $result["ETag"];

        // Shared Origin Access Identity
        $accesIdentityId = $this->originAccessIdentity;

        // Merge settings
        $updatedOrigins = $currentConfig["Origins"];
        $updatedOrigins['Quantity']++;
        $updatedOrigins['Items'][] = $this->getNewOrigins($accesIdentityId);

        $updatedCacheBehaviors = $currentConfig["CacheBehaviors"];
        $updatedCacheBehaviors['Quantity']++;
        $updatedCacheBehaviors['Items'][] = $this->getNewCacheBehaviour();


        $distribution = [
            'CallerReference' => $currentConfig["CallerReference"],
            'Comment' => $currentConfig["Comment"],
            'DefaultCacheBehavior' => $currentConfig["DefaultCacheBehavior"],
            'DefaultRootObject' => $currentConfig["DefaultRootObject"],
            'Enabled' => $currentConfig["Enabled"],
            'Origins' => $updatedOrigins,
            'Aliases' => $currentConfig["Aliases"],
            'CustomErrorResponses' => $currentConfig["CustomErrorResponses"],
            'HttpVersion' => $currentConfig["HttpVersion"],
            'CacheBehaviors' => $updatedCacheBehaviors,
            'Logging' => $currentConfig["Logging"],
            'PriceClass' => $currentConfig["PriceClass"],
            'Restrictions' => $currentConfig["Restrictions"],
            'ViewerCertificate' => $currentConfig["ViewerCertificate"],
            'WebACLId' => $currentConfig["WebACLId"],
        ];

        try {
            $result = $this->cloudfrontClient->updateDistribution([
                'DistributionConfig' => $distribution,
                'Id' => $this->distributionId,
                'IfMatch' => $ETag
            ]);
            //var_dump($result);
            $this->silent ?: WP_CLI::log('Updated cloudfront distribution..');
        } catch (AwsException $e) {
            // output error message if fails
            echo $e->getMessage();
            echo "\n";
            WP_CLI::error('Cloudfront distribution could not be updated. Script stopped.');
            die();
        }

    }



    private function getNewCacheBehaviour(){
        // New settings array for cache
        // https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-cloudfront-2019-03-26.html#updatedistribution
        return [
            'PathPattern' => $this->sitename.'/*',
            'TargetOriginId' => 'S3-'.$this->bucket,
            'ForwardedValues' => [
                'Cookies' => [
                    'Forward' => 'none',
                ],
                'Headers' => [
                    'Items' => ['Origin','Access-Control-Request-Headers'],
                    'Quantity' => 2,

                ],
                'QueryString' => true,
                'QueryStringCacheKeys' => [
                    'Quantity' => 0,
                ],
            ],
            'TrustedSigners' => [
                'Enabled' => false,
                'Quantity' => 0,
            ],
            'ViewerProtocolPolicy' => 'redirect-to-https',
            'MinTTL' => 0,
            'AllowedMethods' => [
                'CachedMethods' => [
                    'Items' => ['HEAD', 'GET'],
                    'Quantity' => 2,
                ],
                'Items' => ['HEAD', 'GET'],
                'Quantity' => 2,
            ],
            'SmoothStreaming' => false,
            'DefaultTTL' => 315360000, // 10 years
            'MaxTTL' => 315360000, // 10 years
            'Compress' => true,
            'LambdaFunctionAssociations' => ['Quantity' => 0],
            'FieldLevelEncryptionId' => '',
        ];
    }


    private function getNewOrigins($accesIdentityId){
        // New settings for origin
        return [
            'DomainName' => $this->bucket . '.s3.amazonaws.com',
            'Id' => 'S3-' . $this->bucket,
            'OriginPath' => '',
            'CustomHeaders' => ['Quantity' => 0],
            'S3OriginConfig' => ['OriginAccessIdentity' => 'origin-access-identity/cloudfront/' . $accesIdentityId],
        ];
    }

    private function getIamPolicy(){
        // This policy is taken from S3 plugin
        // Sets read/write permission only to our bucket
        // https://github.com/humanmade/S3-Uploads/blob/master/inc/class-s3-uploads-wp-cli-command.php#L102
        return '{
          "Version": "2012-10-17",
          "Statement": [
            {
              "Sid": "Stmt1392016154000",
              "Effect": "Allow",
              "Action": [
                "s3:AbortMultipartUpload",
                "s3:DeleteObject",
                "s3:GetBucketAcl",
                "s3:GetBucketLocation",
                "s3:GetBucketPolicy",
                "s3:GetObject",
                "s3:GetObjectAcl",
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:ListMultipartUploadParts",
                "s3:PutObject",
                "s3:PutObjectAcl"
              ],
              "Resource": [
                "arn:aws:s3:::' . $this->bucket . '/*"
              ]
            },
            {
              "Sid": "AllowRootAndHomeListingOfBucket",
              "Action": ["s3:ListBucket"],
              "Effect": "Allow",
              "Resource": ["arn:aws:s3:::' . $this->bucket . '"],
              "Condition":{"StringLike":{"s3:prefix":["*"]}}
            }
          ]
        }';
    }

    private function updateEnv() {
        // Adds Iam credentials to .env file
        $root_dir = dirname(WEBIEN_PLUGIN_PATH, 4);
        $this->rootDir = $root_dir;

        if (file_exists($root_dir.'/.env')) {
            $env = file_get_contents($root_dir.'/.env');

            $env .= PHP_EOL . '# AWS credentials for this bucket' . PHP_EOL;
            $env .= 'AWS_BUCKET='. $this->bucket . PHP_EOL;
            $env .= 'AWS_KEY='. $this->iamKey . PHP_EOL;
            $env .= 'AWS_SECRET='. $this->iamSecret . PHP_EOL;
            $env .= 'AWS_DISTRIBUTION='. $this->distributionUrl . PHP_EOL;
            $env .= 'AWS_ENABLE_UPLOAD=false' . PHP_EOL;
            $env .= '#FORCE_LOCAL_S3_UPLOAD=true #Be careful! (default:false)' . PHP_EOL;
            $output = file_put_contents($root_dir.'/.env', $env);

            if($output){
                $this->silent ?: WP_CLI::log('Updated env file..');
            } else {
                WP_CLI::error('Env file could not be updated.. Script stopped.');
            }
        }
    }

    private function deactivateS3plugin() {
        // Disable S3 plugin for now since we have a bunch of defines that points to cloudfront that's not ready at this stage.
        // Will be activated once we run the wp cli to activate aws setup
        shell_exec("cd $this->rootDir; " . "wp plugin deactivate s3-uploads");
    }


    private function addActivationCron() {
        // If wp cron is active, add cron to activate aws setup once Cloudfront has been deployed.
        if( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ){
            echo PHP_EOL . 'WP CRON is inactive. You need to activate AWS setup manually. (Earliest in 1h)'. PHP_EOL;
            return;
        }

        // Add cron job
    }


    /* This function is called from a separate wp cli command and should be called
     * earliest about 1h after prior aws setup due to cloudfront updates
     */
    function activate($sitename){

        $site = $sitename[0];

        // Check some stuff before activation
        if(!$this->isCloudfrontReady($site)){
            $this->silent ?: WP_CLI::warning( 'Cloudfront is not ready..! Try again soon..' );
            return;
        }
        else if(!defined('AWS_BUCKET')){
            $this->silent ?: WP_CLI::warning( 'There is no aws setup in this installation, env keys missing.' );
            return;
        }

        $root_dir = dirname(WEBIEN_PLUGIN_PATH, 4);
        $bucket = 'webien-'.$site;

        // upload current uploads to S3 with aws cli
        $this->silent ?: WP_CLI::line( 'Uploading local files to S3..' );
        shell_exec("aws s3 sync $root_dir/web/app/uploads s3://" . $bucket . "/" . $site . "/app/uploads");

        // Enable upload/rewrite with S3 plugin by adding auto enable upload to .env (used later as defined variable for plugin)
        if (file_exists($root_dir.'/.env')) {
            $env = file_get_contents($root_dir.'/.env');
            $env = str_replace(["AWS_ENABLE_UPLOAD=false"], ["AWS_ENABLE_UPLOAD=true"], $env);
            $output = file_put_contents($root_dir.'/.env', $env);

            if($output){
                $this->silent ?: WP_CLI::success('Updated env file..');
            } else {
                WP_CLI::error('Env file could not be updated..');
            }

        }

        // Activate optimization checkbox in webien option page
        update_field('optimize_images', 1, 'option');

        // Regenerate Elementor css
        (new \Elementor\Core\Files\Manager())->clear_cache();
    }


    private function isCloudfrontReady($sitename){
        // Do a curl request on a bucket file through cloudfront url and see if it redirects
        // Get index file to test with
        $url = 'https://' . $this->distributionUrl . '/' . $sitename . '/index.html';

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_HEADER, true);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if($code == 307){ // Temporary redirect
            echo 'URL:s was redirected which means Cloudfront is not ready. Testing URL: ' . $url . PHP_EOL;
            return false;
        }
        else if($code == 200){ // OK
            return true;
        }
        else {
            echo 'Cloudfront test did not pass. Testing URL: ' . $url . PHP_EOL;
            return false;
        }
    }



    private function sendAdminMail($msg){
        $SesClient = new SesClient([
            'region'  => 'us-east-1',
            'version' => 'latest',
        ]);

        //$sender_email = 'noreply@webien.cloud';
        $sender_email = 'noreply@webien.cloud';
        $recipient_emails = ['simon@haus.se', 'service@haus.se'];

        $subject = 'Meddelande från Webien/AWS';
        $plaintext_body = $msg ;
        $html_body =  '<h3>Meddelande:</h3>'.
            '<br><p>'.$msg.'</p>'.
            '<br><br><p>Skickat från Webien pluggen som försökt skapa upp det som behövs på Amazon AWS för siten: '.$this->sitename.'</p>';
        $char_set = 'UTF-8';

        try {
            $result = $SesClient->sendEmail([
                'Destination' => [
                    'ToAddresses' => $recipient_emails,
                ],
                'ReplyToAddresses' => [$sender_email],
                'Source' => $sender_email,
                'Message' => [
                    'Body' => [
                        'Html' => [
                            'Charset' => $char_set,
                            'Data' => $html_body,
                        ],
                        'Text' => [
                            'Charset' => $char_set,
                            'Data' => $plaintext_body,
                        ],
                    ],
                    'Subject' => [
                        'Charset' => $char_set,
                        'Data' => $subject,
                    ],
                ],
            ]);
            $messageId = $result['MessageId'];
            //echo("Email sent! Message ID: $messageId"."\n");
            $this->silent ?: WP_CLI::log('Admin email sent..');
        } catch (AwsException $e) {
            // output error message if fails
            echo $e->getMessage();
            echo $e->getAwsErrorMessage();
            echo "\n";
            WP_CLI::error('Admin email could not be sent..');
        }
    }

}


WP_CLI::add_command('aws-setup', 'AwsSetup');