AWSTemplateFormatVersion: "2010-09-09"
Description: >
  WindGuard – Automatically control a TP-Link Kasa outdoor plug based on wind speed.
  Uses AWS Secrets Manager for credentials, SNS for notifications, and inline Lambda code.

Parameters:
  TPLinkEmail:
    Type: String
    Description: TP-Link account email for Kasa Cloud API.
    AllowedPattern: "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
    ConstraintDescription: "Must be a valid email address"
  TPLinkPassword:
    Type: String
    Description: TP-Link account password.
    NoEcho: true
  TPLinkDeviceAlias:
    Type: String
    Description: Alias (friendly name) of the Kasa plug to control.
  OpenWeatherApiKey:
    Type: String
    Description: OpenWeatherMap API key.
    NoEcho: true
  Latitude:
    Type: Number
    Description: Latitude of the plug's location.
  Longitude:
    Type: Number
    Description: Longitude of the plug's location.
  WindOffThreshold:
    Type: Number
    Default: 20
    MinValue: 1
    MaxValue: 100
    Description: Wind speed (mph) above which the plug should turn off.
    ConstraintDescription: "Must be between 1 and 100 mph"
  WindOnThreshold:
    Type: Number
    Default: 10
    MinValue: 1
    MaxValue: 99
    Description: Wind speed (mph) below which the plug should turn back on.
    ConstraintDescription: "Must be between 1 and 99 mph (and less than WindOffThreshold)"
  OnStartHour:
    Type: Number
    Default: 17
    MinValue: 0
    MaxValue: 23
    Description: Hour of day (24h) when the plug schedule starts.
    ConstraintDescription: "Must be between 0 and 23 (24-hour format)"
  OnEndHour:
    Type: Number
    Default: 22
    MinValue: 0
    MaxValue: 23
    Description: Hour of day (24h) when the plug schedule ends.
    ConstraintDescription: "Must be between 0 and 23 (24-hour format)"
  NotifyEmail:
    Type: String
    Default: ""
    Description: Optional email to receive notifications when plug toggles.
  TimeZone:
    Type: String
    Default: "America/New_York"
    AllowedPattern: "^[A-Za-z_]+/[A-Za-z_]+$"
    ConstraintDescription: "Must be a valid timezone (e.g., America/New_York, Europe/London)"
    Description: >
      Timezone for schedule (e.g., America/New_York, America/Los_Angeles, 
      Europe/London). See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

Conditions:
  HasNotifyEmail: !Not [!Equals [!Ref NotifyEmail, ""]]

Resources:
  # --- Secrets Manager Secret ---
  WindGuardSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: windguard-credentials
      Description: TP-Link Kasa credentials and API keys for WindGuard
      SecretString: !Sub |
        {
          "email": "${TPLinkEmail}",
          "password": "${TPLinkPassword}",
          "openweather_api_key": "${OpenWeatherApiKey}"
        }

  # --- DynamoDB State Table ---
  WindGuardStateTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub "${AWS::StackName}-state"
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH

  # --- SNS Topic ---
  WindGuardSNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: "WindGuard Notifications"

  # Optional Email Subscription
  WindGuardSubscription:
    Type: AWS::SNS::Subscription
    Condition: HasNotifyEmail
    Properties:
      Protocol: email
      Endpoint: !Ref NotifyEmail
      TopicArn: !Ref WindGuardSNSTopic

  # --- IAM Role for Lambda ---
  WindGuardRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: windguard-logs
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: "*"
        - PolicyName: windguard-secrets
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource: !Ref WindGuardSecret
        - PolicyName: windguard-sns
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: sns:Publish
                Resource: !Ref WindGuardSNSTopic
        - PolicyName: windguard-dynamodb
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:GetItem
                  - dynamodb:PutItem
                Resource: !GetAtt WindGuardStateTable.Arn

  # --- Lambda Function (inline code) ---
  WindGuardFunction:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: python3.12
      Handler: index.handler
      Timeout: 60
      Role: !GetAtt WindGuardRole.Arn
      Environment:
        Variables:
          SECRET_ARN: !Ref WindGuardSecret
          TPLINK_DEVICE_ALIAS: !Ref TPLinkDeviceAlias
          LAT: !Ref Latitude
          LON: !Ref Longitude
          WIND_OFF_THRESHOLD: !Ref WindOffThreshold
          WIND_ON_THRESHOLD: !Ref WindOnThreshold
          ON_START: !Ref OnStartHour
          ON_END: !Ref OnEndHour
          SNS_TOPIC_ARN: !Ref WindGuardSNSTopic
          TIMEZONE: !Ref TimeZone
          STATE_TABLE: !Ref WindGuardStateTable
      Code:
        ZipFile: |
          import json, os, boto3, datetime
          from datetime import timezone
          from zoneinfo import ZoneInfo
          import urllib.request
          import urllib.parse

          def notify(msg):
              topic = os.environ.get("SNS_TOPIC_ARN")
              if not topic: return
              boto3.client("sns").publish(
                  TopicArn=topic,
                  Subject="WindGuard Update",
                  Message=msg
              )

          def get_secret():
              arn = os.environ["SECRET_ARN"]
              sm = boto3.client("secretsmanager")
              secret = sm.get_secret_value(SecretId=arn)
              return json.loads(secret["SecretString"])

          def get_wind_speed(lat, lon, api_key):
              url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={api_key}&units=imperial"
              with urllib.request.urlopen(url, timeout=5) as response:
                  data = json.loads(response.read())
              return data["wind"].get("speed", 0)

          def make_http_request(url, data=None):
              """Helper function for HTTP requests"""
              if data:
                  data = json.dumps(data).encode('utf-8')
                  req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'})
              else:
                  req = urllib.request.Request(url)
              
              with urllib.request.urlopen(req, timeout=10) as response:
                  return json.loads(response.read())

          def get_device_info():
              """Get current device state and return device info + token"""
              creds = get_secret()
              base = "https://wap.tplinkcloud.com"
              
              # Try Tapo app type first, fallback to Kasa if needed
              login_attempts = [
                  {
                      "method": "login",
                      "params": {
                          "appType": "Tapo_Android",
                          "cloudUserName": creds["email"],
                          "cloudPassword": creds["password"],
                          "terminalUUID": "windguard-" + str(hash(creds["email"]))[-8:]
                      }
                  },
                  {
                      "method": "login", 
                      "params": {
                          "appType": "Kasa_Android",
                          "cloudUserName": creds["email"],
                          "cloudPassword": creds["password"], 
                          "terminalUUID": "windguard-" + str(hash(creds["email"]))[-8:]
                      }
                  }
              ]
              
              login_response = None
              for login_data in login_attempts:
                  try:
                      login_response = make_http_request(base, login_data)
                      if "result" in login_response:
                          break
                  except:
                      continue
              
              if not login_response or "result" not in login_response:
                  error_msg = login_response.get("error_code", "Unknown error")
                  raise Exception(f"TP-Link login failed: {error_msg}. Response: {login_response}")
              
              token = login_response["result"]["token"]
              
              # Get device list
              devices_response = make_http_request(f"{base}?token={token}", {"method": "getDeviceList"})
              if "result" not in devices_response:
                  raise Exception(f"Failed to get device list: {devices_response}")
              
              devices = devices_response["result"]["deviceList"]
              device = next((d for d in devices if d["alias"] == os.environ["TPLINK_DEVICE_ALIAS"]), None)
              if not device:
                  device_names = [d["alias"] for d in devices]
                  raise Exception(f"Device '{os.environ['TPLINK_DEVICE_ALIAS']}' not found. Available devices: {device_names}")
              
              # Get current device state
              status_response = make_http_request(f"{base}?token={token}", {
                  "method": "passthrough",
                  "params": {
                      "deviceId": device["deviceId"],
                      "requestData": json.dumps({"system": {"get_sysinfo": {}}})
                  }
              })
              
              return device, token, status_response

          def tplink_request(method, params):
              device, token, _ = get_device_info()
              base = "https://wap.tplinkcloud.com"
              
              # Send command to device
              command_data = {
                  "method": "passthrough",
                  "params": {
                      "deviceId": device["deviceId"],
                      "requestData": json.dumps({"system": {"set_relay_state": {"state": params["state"]}}})
                  }
              }
              make_http_request(f"{base}?token={token}", command_data)
              return 200  # Success

          def get_current_plug_state():
              """Get the actual current state of the plug"""
              try:
                  _, _, status_response = get_device_info()
                  # Parse the response to get relay state
                  response_data = json.loads(status_response["result"]["responseData"])
                  relay_state = response_data["system"]["get_sysinfo"]["relay_state"]
                  return "ON" if relay_state == 1 else "OFF"
              except Exception as e:
                  print(f"Failed to get plug state: {e}")
                  return None

          def get_local_hour():
              """Get current hour in user's timezone"""
              tz_name = os.environ["TIMEZONE"]
              user_tz = ZoneInfo(tz_name)
              return datetime.datetime.now(user_tz).hour

          def get_last_state():
              """Get the last known plug state from DynamoDB"""
              try:
                  dynamodb = boto3.client('dynamodb')
                  response = dynamodb.get_item(
                      TableName=os.environ['STATE_TABLE'],
                      Key={'id': {'S': 'windguard-state'}}
                  )
                  if 'Item' in response:
                      return response['Item']['state']['S']
                  return None
              except Exception:
                  return None

          def save_state(state):
              """Save current plug state to DynamoDB"""
              try:
                  dynamodb = boto3.client('dynamodb')
                  dynamodb.put_item(
                      TableName=os.environ['STATE_TABLE'],
                      Item={
                          'id': {'S': 'windguard-state'},
                          'state': {'S': state},
                          'timestamp': {'S': str(datetime.datetime.utcnow())}
                      }
                  )
              except Exception as e:
                  print(f"Failed to save state: {e}")

          def handler(event, context):
              try:
                  # Get current time in user's timezone
                  now = get_local_hour()
                  lat, lon = os.environ["LAT"], os.environ["LON"]
                  creds = get_secret()
                  wind = get_wind_speed(lat, lon, creds["openweather_api_key"])
                  off_th = float(os.environ["WIND_OFF_THRESHOLD"])
                  on_th = float(os.environ["WIND_ON_THRESHOLD"])
                  start = int(os.environ["ON_START"])
                  end = int(os.environ["ON_END"])
                  
                  should_be_on = start <= now <= end
                  current_state = get_current_plug_state()
                  last_notified = get_last_state()
                  msg = None

                  if wind > off_th and should_be_on:
                      # Always try to turn off if it's currently on
                      if current_state == "ON":
                          tplink_request("set_relay_state", {"state": 0})
                          # Only notify if we haven't already notified about turning OFF
                          if last_notified != "OFF":
                              save_state("OFF")
                              msg = f"⚠️ WindGuard turned OFF plug (wind={wind:.1f} mph)"
                  elif wind < on_th and should_be_on:
                      # Always try to turn on if it's currently off
                      if current_state == "OFF":
                          tplink_request("set_relay_state", {"state": 1})
                          # Only notify if we haven't already notified about turning ON
                          if last_notified != "ON":
                              save_state("ON")
                              msg = f"✅ WindGuard re-enabled plug (wind={wind:.1f} mph)"
                  
                  if msg:
                      notify(msg)
                  return {"statusCode": 200, "body": json.dumps({"wind": wind, "current_state": current_state, "last_notified": last_notified})}
              
              except Exception as e:
                  error_msg = f"WindGuard error: {str(e)}"
                  print(error_msg)  # CloudWatch logs
                  notify(f"🚨 {error_msg}")
                  return {"statusCode": 500, "body": json.dumps({"error": str(e)})}

  # --- EventBridge Schedule ---
  WindGuardSchedule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: rate(5 minutes)
      Targets:
        - Arn: !GetAtt WindGuardFunction.Arn
          Id: WindGuardTarget

  # --- Lambda Invoke Permission ---
  WindGuardPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref WindGuardFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt WindGuardSchedule.Arn

Outputs:
  LambdaFunctionName:
    Value: !Ref WindGuardFunction
  SNSTopicArn:
    Value: !Ref WindGuardSNSTopic
  SecretArn:
    Value: !Ref WindGuardSecret
  StateTableName:
    Value: !Ref WindGuardStateTable
