削除

#!/usr/bin/env python3
"""
AWS S3関連リソース安全削除スクリプト（改良版）
S3バケットに関連するCloudFront、Route53、CloudFront Functionを安全に削除します
複数環境対応・厳格な関連性チェック・適切な削除順序を実装
"""

import boto3
import time
import sys
from botocore.exceptions import ClientError, NoCredentialsError
import json
import re
from urllib.parse import urlparse

def print_banner(text):
    """バナー表示用の関数"""
    print("=" * 70)
    print(f" {text}")
    print("=" * 70)

def print_section(text):
    """セクション表示用の関数"""
    print(f"\n--- {text} ---")

def print_warning(text):
    """警告表示用の関数"""
    print(f"⚠️  WARNING: {text}")

def print_error(text):
    """エラー表示用の関数"""
    print(f"❌ ERROR: {text}")

def print_success(text):
    """成功表示用の関数"""
    print(f"✅ {text}")

def print_info(text):
    """情報表示用の関数"""
    print(f"ℹ️  {text}")

def get_aws_clients():
    """AWSクライアントを取得"""
    try:
        s3 = boto3.client('s3')
        cloudfront = boto3.client('cloudfront')
        route53 = boto3.client('route53')
        
        # 認証テスト
        s3.list_buckets()
        print_success("AWS認証が確認できました")
        return s3, cloudfront, route53
    except NoCredentialsError:
        print_error("AWS認証情報が設定されていません")
        sys.exit(1)
    except Exception as e:
        print_error(f"AWS接続エラー: {e}")
        sys.exit(1)

def validate_bucket_exists(s3, bucket_name):
    """S3バケットの存在確認と詳細情報取得"""
    try:
        # バケット存在確認
        s3.head_bucket(Bucket=bucket_name)
        
        # バケット情報取得
        bucket_info = {}
        
        # リージョン取得
        try:
            location = s3.get_bucket_location(Bucket=bucket_name)
            bucket_info['region'] = location.get('LocationConstraint', 'us-east-1')
        except Exception:
            bucket_info['region'] = 'unknown'
        
        # バケットポリシー確認（静的ウェブサイトか判定）
        try:
            website_config = s3.get_bucket_website(Bucket=bucket_name)
            bucket_info['is_website'] = True
            bucket_info['index_document'] = website_config.get('IndexDocument', {}).get('Suffix', 'index.html')
        except ClientError:
            bucket_info['is_website'] = False
        
        # オブジェクト数確認
        try:
            response = s3.list_objects_v2(Bucket=bucket_name, MaxKeys=1)
            bucket_info['has_objects'] = 'Contents' in response
        except Exception:
            bucket_info['has_objects'] = False
        
        print_success(f"S3バケット '{bucket_name}' が見つかりました")
        print_info(f"リージョン: {bucket_info['region']}")
        print_info(f"静的ウェブサイト: {'有効' if bucket_info['is_website'] else '無効'}")
        print_info(f"オブジェクト: {'あり' if bucket_info['has_objects'] else 'なし'}")
        
        return True, bucket_info
    except ClientError as e:
        error_code = e.response['Error']['Code']
        if error_code == '404':
            print_error(f"S3バケット '{bucket_name}' が見つかりません")
        elif error_code == '403':
            print_error(f"S3バケット '{bucket_name}' へのアクセス権限がありません")
        else:
            print_error(f"バケット確認エラー: {e}")
        return False, {}

def get_bucket_domain_patterns(bucket_name, bucket_info):
    """バケットに関連するドメインパターンを生成"""
    patterns = set()
    
    # 標準的なS3ドメインパターン
    patterns.add(f"{bucket_name}.s3.amazonaws.com")
    patterns.add(f"{bucket_name}.s3-website.{bucket_info.get('region', 'us-east-1')}.amazonaws.com")
    patterns.add(f"{bucket_name}.s3-website-{bucket_info.get('region', 'us-east-1')}.amazonaws.com")
    
    # リージョン別パターン
    if bucket_info.get('region') and bucket_info['region'] != 'us-east-1':
        patterns.add(f"{bucket_name}.s3.{bucket_info['region']}.amazonaws.com")
    
    return patterns

def find_related_cloudfront_distributions(cloudfront, bucket_name, bucket_info):
    """関連するCloudFrontディストリビューションを厳密に検索"""
    print_section("CloudFrontディストリビューション検索中...")
    
    try:
        response = cloudfront.list_distributions()
        related_distributions = []
        bucket_patterns = get_bucket_domain_patterns(bucket_name, bucket_info)
        
        if 'DistributionList' in response and 'Items' in response['DistributionList']:
            for dist in response['DistributionList']['Items']:
                is_related = False
                matched_origins = []
                
                # 各オリジンをチェック
                for origin in dist['Origins']['Items']:
                    origin_domain = origin['DomainName'].lower()
                    
                    # 厳密なマッチング
                    for pattern in bucket_patterns:
                        if pattern.lower() == origin_domain:
                            is_related = True
                            matched_origins.append(origin_domain)
                            break
                
                if is_related:
                    # 詳細情報を取得
                    dist_detail = cloudfront.get_distribution(Id=dist['Id'])
                    config = dist_detail['Distribution']['DistributionConfig']
                    
                    related_distributions.append({
                        'Id': dist['Id'],
                        'DomainName': dist['DomainName'],
                        'Status': dist['Status'],
                        'Enabled': dist['Enabled'],
                        'MatchedOrigins': matched_origins,
                        'Comment': config.get('Comment', ''),
                        'Aliases': [alias for alias in config.get('Aliases', {}).get('Items', [])],
                        'ETag': dist_detail['ETag']
                    })
        
        if related_distributions:
            print_success(f"{len(related_distributions)}個の関連ディストリビューションを発見")
        else:
            print_info("関連するCloudFrontディストリビューションは見つかりませんでした")
        
        return related_distributions
    except ClientError as e:
        print_error(f"CloudFront検索エラー: {e}")
        return []

def find_related_cloudfront_functions(cloudfront, bucket_name, distributions):
    """関連するCloudFront Functionを検索（ディストリビューションとの関連性も考慮）"""
    print_section("CloudFront Function検索中...")
    
    try:
        response = cloudfront.list_functions()
        related_functions = []
        
        if 'FunctionList' in response and 'Items' in response['FunctionList']:
            for func in response['FunctionList']['Items']:
                is_related = False
                relation_reason = ""
                
                # 1. 関数名にバケット名が含まれているかチェック
                if bucket_name.lower() in func['Name'].lower():
                    is_related = True
                    relation_reason = f"関数名にバケット名'{bucket_name}'が含まれている"
                
                # 2. 関連ディストリビューションで使用されているかチェック
                if not is_related and distributions:
                    for dist in distributions:
                        try:
                            # ディストリビューション設定から関数の使用状況を確認
                            dist_detail = cloudfront.get_distribution_config(Id=dist['Id'])
                            config = dist_detail['DistributionConfig']
                            
                            # デフォルトキャッシュビヘイビアの関数をチェック
                            default_behavior = config.get('DefaultCacheBehavior', {})
                            function_associations = default_behavior.get('FunctionAssociations', {}).get('Items', [])
                            
                            for assoc in function_associations:
                                if assoc.get('FunctionARN', '').endswith(f"function/{func['Name']}"):
                                    is_related = True
                                    relation_reason = f"ディストリビューション'{dist['Id']}'で使用されている"
                                    break
                            
                            if is_related:
                                break
                                
                        except Exception as e:
                            # エラーは無視して次へ
                            continue
                
                if is_related:
                    # 関数の詳細情報を取得
                    try:
                        func_detail = cloudfront.describe_function(Name=func['Name'])
                        related_functions.append({
                            'Name': func['Name'],
                            'Status': func['Status'],
                            'Stage': func.get('Stage', 'DEVELOPMENT'),
                            'RelationReason': relation_reason,
                            'ETag': func_detail['ETag']
                        })
                    except Exception:
                        # 詳細情報取得に失敗した場合は基本情報のみ
                        related_functions.append({
                            'Name': func['Name'],
                            'Status': func['Status'],
                            'Stage': func.get('Stage', 'DEVELOPMENT'),
                            'RelationReason': relation_reason,
                            'ETag': None
                        })
        
        if related_functions:
            print_success(f"{len(related_functions)}個の関連CloudFront Functionを発見")
        else:
            print_info("関連するCloudFront Functionは見つかりませんでした")
        
        return related_functions
    except ClientError as e:
        print_error(f"CloudFront Function検索エラー: {e}")
        return []

def get_hosted_zone_id():
    """固定のホストゾーンIDを取得"""
    print_section("Route53ホストゾーン設定")
    
    # ホストゾーンIDを手動入力
    while True:
        zone_id = input("Route53ホストゾーンID（Z0123456789ABCDEFGHIJ形式）を入力してください（なしの場合はEnter）: ").strip()
        
        if not zone_id:
            print_info("Route53の検索をスキップします")
            return None
            
        # ホストゾーンIDの形式チェック
        if re.match(r'^Z[A-Z0-9]{10,32}$', zone_id):
            return zone_id
        else:
            print_error("ホストゾーンIDの形式が正しくありません（例: Z0123456789ABCDEFGHIJ）")

def find_related_route53_records(route53, hosted_zone_id, bucket_name, distributions):
    """関連するRoute53 Aレコードを検索（指定されたホストゾーンのみ）"""
    if not hosted_zone_id:
        return []
    
    print_section("Route53 Aレコード検索中...")
    
    try:
        # ホストゾーン情報を取得
        zone_info = route53.get_hosted_zone(Id=hosted_zone_id)
        zone_name = zone_info['HostedZone']['Name']
        
        print_info(f"ホストゾーン '{zone_name}' を検索中...")
        
        records = route53.list_resource_record_sets(HostedZoneId=hosted_zone_id)
        related_records = []
        
        # CloudFrontディストリビューションのドメイン名を収集
        cf_domains = set()
        for dist in distributions:
            cf_domains.add(dist['DomainName'].lower())
            for alias in dist.get('Aliases', []):
                cf_domains.add(alias.lower())
        
        for record in records['ResourceRecordSets']:
            if record['Type'] == 'A':
                is_related = False
                relation_reason = ""
                
                # ALIASレコードの場合（CloudFront向け）
                if 'AliasTarget' in record:
                    alias_target = record['AliasTarget']['DNSName'].lower()
                    
                    # CloudFrontディストリビューションへのエイリアスかチェック
                    for cf_domain in cf_domains:
                        if cf_domain in alias_target:
                            is_related = True
                            relation_reason = f"CloudFrontディストリビューション'{cf_domain}'へのエイリアス"
                            break
                
                # 通常のAレコードの場合
                elif 'ResourceRecords' in record:
                    for rr in record['ResourceRecords']:
                        value = rr.get('Value', '').lower()
                        # バケットのエンドポイントと一致するかチェック
                        if bucket_name.lower() in value:
                            is_related = True
                            relation_reason = f"バケット'{bucket_name}'のエンドポイントを参照"
                            break
                
                if is_related:
                    related_records.append({
                        'ZoneId': hosted_zone_id,
                        'ZoneName': zone_name,
                        'Name': record['Name'],
                        'Type': record['Type'],
                        'TTL': record.get('TTL'),
                        'AliasTarget': record.get('AliasTarget'),
                        'ResourceRecords': record.get('ResourceRecords'),
                        'RelationReason': relation_reason
                    })
        
        if related_records:
            print_success(f"{len(related_records)}個の関連Aレコードを発見")
        else:
            print_info("関連するRoute53 Aレコードは見つかりませんでした")
        
        return related_records
    except ClientError as e:
        print_error(f"Route53検索エラー: {e}")
        return []

def display_resources_summary(bucket_name, bucket_info, distributions, functions, route53_records):
    """削除対象リソースの詳細一覧表示"""
    print_banner("削除対象リソース詳細一覧")
    
    print(f"🪣 S3バケット: {bucket_name}")
    print(f"   リージョン: {bucket_info.get('region', 'unknown')}")
    print(f"   静的ウェブサイト: {'有効' if bucket_info.get('is_website') else '無効'}")
    print(f"   オブジェクト: {'あり' if bucket_info.get('has_objects') else 'なし'}")
    
    print(f"\n☁️ CloudFrontディストリビューション ({len(distributions)}個):")
    if distributions:
        for i, dist in enumerate(distributions, 1):
            status_icon = "🟢" if dist['Status'] == 'Deployed' else "🟡"
            enabled_icon = "✅" if dist['Enabled'] else "❌"
            print(f"  {i}. {status_icon} ID: {dist['Id']}")
            print(f"     ドメイン: {dist['DomainName']}")
            print(f"     有効: {enabled_icon} | ステータス: {dist['Status']}")
            print(f"     コメント: {dist.get('Comment', 'なし')}")
            if dist.get('Aliases'):
                print(f"     エイリアス: {', '.join(dist['Aliases'])}")
            print(f"     関連オリジン: {', '.join(dist['MatchedOrigins'])}")
            print()
    else:
        print("  なし")
    
    print(f"\n⚡ CloudFront Function ({len(functions)}個):")
    if functions:
        for i, func in enumerate(functions, 1):
            print(f"  {i}. 📦 名前: {func['Name']}")
            print(f"     ステータス: {func['Status']} | ステージ: {func['Stage']}")
            print(f"     関連理由: {func['RelationReason']}")
            print()
    else:
        print("  なし")
    
    print(f"\n🌐 Route53 Aレコード ({len(route53_records)}個):")
    if route53_records:
        for i, record in enumerate(route53_records, 1):
            print(f"  {i}. 📝 名前: {record['Name']}")
            print(f"     ゾーン: {record['ZoneName']}")
            print(f"     TTL: {record.get('TTL', 'エイリアス')}")
            print(f"     関連理由: {record['RelationReason']}")
            if record.get('AliasTarget'):
                print(f"     エイリアス先: {record['AliasTarget']['DNSName']}")
            elif record.get('ResourceRecords'):
                values = [rr['Value'] for rr in record['ResourceRecords']]
                print(f"     値: {', '.join(values)}")
            print()
    else:
        print("  なし")

def confirm_deletion():
    """削除確認（多段階）"""
    print_section("削除確認")
    
    # 1段階目：概要確認
    print_warning("この操作は元に戻すことができません！")
    print_info("削除される内容を再度確認してください")
    
    while True:
        response1 = input("上記のリソースを削除してもよろしいですか？ (yes/no): ").lower().strip()
        if response1 in ['yes', 'y']:
            break
        elif response1 in ['no', 'n']:
            return False
        else:
            print("'yes' または 'no' で回答してください")
    
    # 2段階目：最終確認
    print_warning("最終確認です")
    confirmation_text = "DELETE"
    user_input = input(f"削除を実行するには '{confirmation_text}' と入力してください: ").strip()
    
    if user_input == confirmation_text:
        print_success("削除が確認されました")
        return True
    else:
        print_error("確認文字列が一致しません。削除をキャンセルします")
        return False

def delete_route53_records(route53, records):
    """Route53 Aレコードを削除（最初に実行）"""
    if not records:
        return True
    
    print_section("【1/5】Route53 Aレコード削除中...")
    success_count = 0
    
    for record in records:
        try:
            print(f"🗑️ Aレコード '{record['Name']}' を削除中...")
            
            # 削除用のレコードセット構築
            if record.get('AliasTarget'):
                # エイリアスレコードの場合
                record_set = {
                    'Name': record['Name'],
                    'Type': record['Type'],
                    'AliasTarget': record['AliasTarget']
                }
            else:
                # 通常のAレコードの場合
                record_set = {
                    'Name': record['Name'],
                    'Type': record['Type'],
                    'TTL': record.get('TTL', 300),
                    'ResourceRecords': record.get('ResourceRecords', [])
                }
            
            change_batch = {
                'Changes': [{
                    'Action': 'DELETE',
                    'ResourceRecordSet': record_set
                }]
            }
            
            response = route53.change_resource_record_sets(
                HostedZoneId=record['ZoneId'],
                ChangeBatch=change_batch
            )
            
            print_success(f"Aレコード '{record['Name']}' を削除しました")
            success_count += 1
            
        except ClientError as e:
            print_error(f"Aレコード '{record['Name']}' 削除エラー: {e}")
    
    print_info(f"Route53レコード削除完了: {success_count}/{len(records)}")
    return success_count == len(records)

def delete_cloudfront_functions(cloudfront, functions):
    """CloudFront Functionを削除（2番目に実行）"""
    if not functions:
        return True
    
    print_section("【2/5】CloudFront Function削除中...")
    success_count = 0
    
    for func in functions:
        try:
            print(f"🗑️ Function '{func['Name']}' を削除中...")
            
            # 関数がLIVEステージにある場合は、まずDEVELOPMENTに戻す
            if func.get('Stage') == 'LIVE':
                print_info(f"Function '{func['Name']}' をDEVELOPMENTステージに戻しています...")
                # 実際の実装では、関数をLIVEから削除する処理が必要
                # ここでは簡略化
            
            cloudfront.delete_function(Name=func['Name'])
            print_success(f"Function '{func['Name']}' を削除しました")
            success_count += 1
            
        except ClientError as e:
            error_code = e.response['Error']['Code']
            if error_code == 'FunctionInUse':
                print_error(f"Function '{func['Name']}' は使用中のため削除できません")
            else:
                print_error(f"Function '{func['Name']}' 削除エラー: {e}")
    
    print_info(f"CloudFront Function削除完了: {success_count}/{len(functions)}")
    return success_count == len(functions)

def disable_cloudfront_distributions(cloudfront, distributions):
    """CloudFrontディストリビューションを無効化（3番目に実行）"""
    if not distributions:
        return True
    
    print_section("【3/5】CloudFrontディストリビューション無効化中...")
    success_count = 0
    
    for dist in distributions:
        if not dist['Enabled']:
            print_info(f"ディストリビューション '{dist['Id']}' は既に無効化されています")
            success_count += 1
            continue
            
        try:
            print(f"⏸️ ディストリビューション '{dist['Id']}' を無効化中...")
            
            # 現在の設定を取得
            response = cloudfront.get_distribution_config(Id=dist['Id'])
            config = response['DistributionConfig']
            etag = response['ETag']
            
            # 無効化
            config['Enabled'] = False
            
            cloudfront.update_distribution(
                Id=dist['Id'],
                DistributionConfig=config,
                IfMatch=etag
            )
            
            print_success(f"ディストリビューション '{dist['Id']}' を無効化しました")
            success_count += 1
            
        except ClientError as e:
            print_error(f"ディストリビューション '{dist['Id']}' 無効化エラー: {e}")
    
    print_info(f"CloudFrontディストリビューション無効化完了: {success_count}/{len(distributions)}")
    return success_count == len(distributions)

def wait_for_cloudfront_deployments(cloudfront, distributions):
    """CloudFrontディストリビューションのデプロイ完了を待機"""
    if not distributions:
        return True
    
    print_section("【4/5】CloudFrontディストリビューションのデプロイ完了待機中...")
    
    all_deployed = False
    max_wait_time = 3600  # 最大60分待機
    wait_interval = 30    # 30秒間隔でチェック
    total_waited = 0
    
    while not all_deployed and total_waited < max_wait_time:
        all_deployed = True
        
        for dist in distributions:
            try:
                response = cloudfront.get_distribution(Id=dist['Id'])
                status = response['Distribution']['Status']
                
                if status != 'Deployed':
                    all_deployed = False
                    print(f"⏳ ディストリビューション '{dist['Id']}': {status}")
                else:
                    print(f"✅ ディストリビューション '{dist['Id']}': デプロイ完了")
                    
            except ClientError as e:
                print_error(f"ステータス確認エラー ({dist['Id']}): {e}")
                all_deployed = False
        
        if not all_deployed:
            print_info(f"次のチェックまで{wait_interval}秒待機... (経過時間: {total_waited//60}分)")
            time.sleep(wait_interval)
            total_waited += wait_interval
    
    if all_deployed:
        print_success("すべてのディストリビューションのデプロイが完了しました")
        return True
    else:
        print_warning(f"タイムアウト: {max_wait_time//60}分経過しました")
        
        # 続行するか確認
        while True:
            response = input("デプロイ完了を待たずに削除を続行しますか？ (yes/no): ").lower().strip()
            if response in ['yes', 'y']:
                return True
            elif response in ['no', 'n']:
                print_error("削除処理を中断します")
                return False
            else:
                print("'yes' または 'no' で回答してください")

def delete_cloudfront_distributions(cloudfront, distributions):
    """CloudFrontディストリビューションを削除（5番目に実行）"""
    if not distributions:
        return True
    
    print_section("【5/5】CloudFrontディストリビューション削除中...")
    success_count = 0
    
    for dist in distributions:
        try:
            print(f"🗑️ ディストリビューション '{dist['Id']}' を削除中...")
            
            # 最新のETagを取得
            response = cloudfront.get_distribution_config(Id=dist['Id'])
            etag = response['ETag']
            
            cloudfront.delete_distribution(Id=dist['Id'], IfMatch=etag)
            print_success(f"ディストリビューション '{dist['Id']}' を削除しました")
            success_count += 1
            
        except ClientError as e:
            error_code = e.response['Error']['Code']
            if error_code == 'DistributionNotDisabled':
                print_error(f"ディストリビューション '{dist['Id']}' がまだ有効化されています")
            elif error_code == 'PreconditionFailed':
                print_error(f"ディストリビューション '{dist['Id']}' のETagが一致しません")
            else:
                print_error(f"ディストリビューション '{dist['Id']}' 削除エラー: {e}")
    
    print_info(f"CloudFrontディストリビューション削除完了: {success_count}/{len(distributions)}")
    return success_count == len(distributions)

def empty_and_delete_s3_bucket(s3, bucket_name):
    """S3バケットを空にして削除（最後に実行）"""
    print_section("【6/6】S3バケット削除中...")
    
    try:
        # バケット内容削除
        print(f"📦 バケット '{bucket_name}' の内容を削除中...")
        
        # すべてのオブジェクトを削除
        paginator = s3.get_paginator('list_objects_v2')
        pages = paginator.paginate(Bucket=bucket_name)
        
        delete_keys = []
        for page in pages:
            if 'Contents' in page:
                for obj in page['Contents']:
                    delete_keys.append({'Key': obj['Key']})
        
        if delete_keys:
            # 1000個ずつ削除（AWS制限）
            for i in range(0, len(delete_keys), 1000):
                batch = delete_keys[i:i+1000]
                s3.delete_objects(
                    Bucket=bucket_name,
                    Delete={'Objects': batch}
                )
                print_info(f"削除完了: {len(batch)}個のオブジェクト")
            
            print_success(f"バケット内の全オブジェクト（{len(delete_keys)}個）を削除しました")
        else:
            print_info("バケットは既に空です")
        
        # バージョン管理されたオブジェクトも削除
        try:
            versions_paginator = s3.get_paginator('list_object_versions')
            versions_pages = versions_paginator.paginate(Bucket=bucket_name)
            
            version_delete_keys = []
            for page in versions_pages:
                if 'Versions' in page:
                    for version in page['Versions']:
                        version_delete_keys.append({
                            'Key': version['Key'],
                            'VersionId': version['VersionId']
                        })
                if 'DeleteMarkers' in page:
                    for marker in page['DeleteMarkers']:
                        version_delete_keys.append({
                            'Key': marker['Key'],
                            'VersionId': marker['VersionId']
                        })
            
            if version_delete_keys:
                # 1000個ずつ削除（AWS制限）
                for i in range(0, len(version_delete_keys), 1000):
                    batch = version_delete_keys[i:i+1000]
                    s3.delete_objects(
                        Bucket=bucket_name,
                        Delete={'Objects': batch}
                    )
                    print_info(f"バージョン削除完了: {len(batch)}個")
                
                print_success(f"バケット内の全バージョン（{len(version_delete_keys)}個）を削除しました")
        
        except ClientError as e:
            if e.response['Error']['Code'] != 'NoSuchBucket':
                print_info("バージョン管理は無効化されています")
        
        # バケット削除
        print(f"🗑️ バケット '{bucket_name}' を削除中...")
        s3.delete_bucket(Bucket=bucket_name)
        print_success(f"バケット '{bucket_name}' を削除しました")
        return True
        
    except ClientError as e:
        error_code = e.response['Error']['Code']
        if error_code == 'BucketNotEmpty':
            print_error(f"バケット '{bucket_name}' が空ではありません")
        elif error_code == 'NoSuchBucket':
            print_info(f"バケット '{bucket_name}' は既に削除されています")
            return True
        else:
            print_error(f"バケット削除エラー: {e}")
        return False

def main():
    print_banner("AWS S3関連リソース安全削除スクリプト（改良版）")
    
    # バケット名の入力と検証
    while True:
        bucket_name = input("削除するS3バケット名を入力してください: ").strip()
        if not bucket_name:
            print_error("バケット名が入力されていません")
            continue
        
        # バケット名の形式チェック
        if not re.match(r'^[a-z0-9.-]+
                            ', bucket_name):
            print_error("バケット名の形式が正しくありません（小文字、数字、ハイフン、ピリオドのみ）")
            continue
        
        break
    
    # AWSクライアント取得
    s3, cloudfront, route53 = get_aws_clients()
    
    # バケットの存在確認と詳細情報取得
    bucket_exists, bucket_info = validate_bucket_exists(s3, bucket_name)
    if not bucket_exists:
        sys.exit(1)
    
    # 関連リソースの検索
    print_banner("関連リソース検索中...")
    
    distributions = find_related_cloudfront_distributions(cloudfront, bucket_name, bucket_info)
    functions = find_related_cloudfront_functions(cloudfront, bucket_name, distributions)
    
    # Route53ホストゾーンIDの取得
    hosted_zone_id = get_hosted_zone_id()
    route53_records = find_related_route53_records(route53, hosted_zone_id, bucket_name, distributions)
    
    # リソース一覧表示
    display_resources_summary(bucket_name, bucket_info, distributions, functions, route53_records)
    
    # 削除順序の説明
    print_banner("削除順序について")
    print("以下の順序で安全に削除を実行します：")
    print("1. Route53 Aレコード（DNS参照を停止）")
    print("2. CloudFront Function（関数の依存関係を解除）") 
    print("3. CloudFrontディストリビューション無効化（配信を停止）")
    print("4. CloudFrontデプロイ完了待機（完全に停止まで待機）")
    print("5. CloudFrontディストリビューション削除")
    print("6. S3バケット内容削除→バケット削除")
    
    # 削除確認
    if not confirm_deletion():
        print_error("削除をキャンセルしました")
        sys.exit(0)
    
    # 削除実行
    print_banner("リソース削除開始")
    
    all_success = True
    
    # 1. Route53レコード削除
    if not delete_route53_records(route53, route53_records):
        all_success = False
        print_warning("Route53レコード削除で一部エラーが発生しました")
    
    # 2. CloudFront Function削除
    if not delete_cloudfront_functions(cloudfront, functions):
        all_success = False
        print_warning("CloudFront Function削除で一部エラーが発生しました")
    
    # 3. CloudFrontディストリビューション無効化
    if not disable_cloudfront_distributions(cloudfront, distributions):
        all_success = False
        print_warning("CloudFrontディストリビューション無効化で一部エラーが発生しました")
    
    # 4. CloudFrontデプロイ完了待機
    if not wait_for_cloudfront_deployments(cloudfront, distributions):
        print_error("削除処理を中断しました")
        sys.exit(1)
    
    # 5. CloudFrontディストリビューション削除
    if not delete_cloudfront_distributions(cloudfront, distributions):
        all_success = False
        print_warning("CloudFrontディストリビューション削除で一部エラーが発生しました")
    
    # 6. S3バケット削除
    if not empty_and_delete_s3_bucket(s3, bucket_name):
        all_success = False
        print_error("S3バケット削除でエラーが発生しました")
    
    # 結果報告
    print_banner("削除結果")
    if all_success:
        print_success("🎉 全てのリソースが正常に削除されました！")
    else:
        print_warning("⚠️ 一部のリソース削除でエラーが発生しました")
        print_info("AWS Management Consoleで残存リソースを確認してください")
    
    print_info("削除処理が完了しました")

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print_error("\n削除処理が中断されました")
        sys.exit(1)
    except Exception as e:
        print_error(f"予期しないエラーが発生しました: {e}")
        sys.exit(1)
                            '