from flask import Blueprint, request, jsonify, session, current_app
from flask_login import login_required, current_user
from src.models.user import db, User
from src.models.subscription import Product, Subscription, Payment, SubscriptionStatus, PaymentStatus, PaymentMethod, UserProfile
from src.models.payment import PaymentHistory, PaymentStatus as PHStatus
from datetime import datetime, timedelta, timezone, date
from calendar import monthrange
import json
import base64
import hashlib
import hmac
import logging
import time
import uuid, requests, os
from src.config.env_loader import env
from src.utils.user_ref import resolve_user_ref

subscription_bp = Blueprint('subscription', __name__)
logger = logging.getLogger(__name__)

def _add_one_month_same_day(base_date: date, start_day: int) -> date:
    """base_date 기준으로 +1개월, 시작일의 '일(day)' 유지. 말일 보정."""
    # 다음 달 임시 날짜
    if base_date.month == 12:
        y, m = base_date.year + 1, 1
    else:
        y, m = base_date.year, base_date.month + 1
    
    # 다음 달 말일
    last_day = monthrange(y, m)[1]
    keep_day = min(start_day, last_day)
    return date(y, m, keep_day)

def _calculate_subscription_credit_fields(start_date: datetime, billing_cycle: str):
    """구독 생성 시 크레딧 관련 필드 계산
    Returns:
        tuple: (prepaid_cycles_remaining, next_credit_date, last_credit_date)
    """
    # prepaid_cycles_remaining: 연간 선결제면 12, 월간이면 0
    prepaid_cycles_remaining = 12 if billing_cycle == 'yearly' else 0
    
    # next_credit_date: 시작일 기준으로 다음 달 같은 날짜 계산
    start_date_kst = start_date.replace(tzinfo=timezone(timedelta(hours=9))) if start_date.tzinfo is None else start_date
    start_date_local = start_date_kst.astimezone(timezone(timedelta(hours=9)))
    start_day = start_date_local.day
    start_date_only = start_date_local.date()
    next_credit_date = _add_one_month_same_day(start_date_only, start_day)
    
    # last_credit_date: 구독 생성 시점에는 NULL (아직 지급되지 않음)
    last_credit_date = None
    
    return prepaid_cycles_remaining, next_credit_date, last_credit_date

def _reset_subscription_credit_fields(subscription: Subscription):
    """구독 취소 시 크레딧 관련 필드 초기화
    프로시저 sp_renew_subscription_credits_one이 더 이상 실행되지 않도록 필드 초기화
    - next_credit_date를 NULL로 설정하여 프로시저가 실행되지 않도록 함
    - last_credit_date를 NULL로 초기화
    - prepaid_cycles_remaining을 0으로 초기화
    """
    subscription.next_credit_date = None
    subscription.last_credit_date = None
    subscription.prepaid_cycles_remaining = 0
    logger.info(f"구독 {subscription.id}의 크레딧 관련 필드 초기화 완료 (프로시저 실행 방지)")

def call_renew_subscription_credits_procedure(subscription_id: int):
    """프로시저 sp_renew_subscription_credits_one 호출 (선택사항)
    필요시 구독 크레딧 갱신을 위해 프로시저를 직접 호출할 수 있습니다.
    """
    try:
        from sqlalchemy import text
        result = db.session.execute(
            text("CALL sp_renew_subscription_credits_one(:subscription_id)"),
            {"subscription_id": subscription_id}
        )
        db.session.commit()
        logger.info(f"프로시저 sp_renew_subscription_credits_one 호출 완료: subscription_id={subscription_id}")
        return {"success": True}
    except Exception as e:
        db.session.rollback()
        logger.error(f"프로시저 호출 실패: subscription_id={subscription_id}, error={str(e)}")
        return {"success": False, "error": str(e)}

# 환경에 따라 URL 동적 설정
def get_front_url():
    """프론트엔드 URL 반환 (환경별)"""
    front_url = os.environ.get('FRONT_URL')
    if front_url:
        return front_url
    # 환경 변수가 없으면 환경에 따라 기본값 설정
    if env.is_production:
        return 'https://mlink.sellmall.co.kr'
    return 'http://localhost:3001'

def get_back_url():
    """백엔드 URL 반환 (환경별)"""
    back_url = os.environ.get('BACK_URL')
    if back_url:
        return back_url
    # 환경 변수가 없으면 환경에 따라 기본값 설정
    if env.is_production:
        return 'https://mlink.sellmall.co.kr'
    return 'http://localhost:8011'

FRONT_URL = get_front_url()
BACK_URL = get_back_url()

# 로깅으로 URL 확인
logger.info(f"환경 설정: ENVIRONMENT={os.environ.get('ENVIRONMENT', 'development')}, FRONT_URL={FRONT_URL}, BACK_URL={BACK_URL}")

TOSS_CONFIG = {
    'api_key': os.environ.get('TOSS_API_KEY'),
    'base_url': 'https://pay.toss.im/api/v2/payments',
    'refund_url': 'https://pay.toss.im/api/v2/refunds',  # 복수형으로 통일
    'status_url': 'https://pay.toss.im/api/v2/status',
    'execute_url': 'https://pay.toss.im/api/v2/execute'
}

# =========================
# 상품 목록
# =========================
@subscription_bp.route('/api/subscription/products', methods=['GET'])
def get_products():
    """상품 목록 조회"""
    try:
        billing_cycle = request.args.get('billing_cycle', 'monthly')  # 기본값은 monthly
        
        # billing_cycle에 따라 상품 필터링
        if billing_cycle == 'monthly':
            products = Product.query.filter_by(is_active=True, billing_cycle='monthly').all()
        elif billing_cycle == 'yearly':
            products = Product.query.filter_by(is_active=True, billing_cycle='yearly').all()
        else:
            products = Product.query.filter_by(is_active=True).all()
        
        def safe_parse_features(features_string):
            """features JSON 파싱 (파싱 실패 시 빈 배열 반환)"""
            if not features_string:
                return []
            try:
                parsed = json.loads(features_string)
                return parsed if isinstance(parsed, list) else []
            except (json.JSONDecodeError, TypeError) as e:
                logger.warning(f"features JSON 파싱 실패: {e}, 원본 데이터: {features_string}")
                return []
        
        return jsonify([{
            'id': p.id,
            'name': p.name,
            'description': p.description,
            'price': p.price,
            'currency': p.currency,
            'billing_cycle': p.billing_cycle,
            'trial_days': p.trial_days,
            'features': safe_parse_features(p.features)
        } for p in products]), 200
    except Exception as e:
        logger.exception("get_products failed")
        return jsonify({'error': str(e)}), 500

# =========================
# (선택) 구독 선 생성 — 정기 비활성화 상태에서도 유지
# =========================
@subscription_bp.route('/api/subscription/subscribe', methods=['POST'])
def create_subscription():
    """구독 생성 (결제 없이 선 생성 가능) — auto_renew 비활성화
    보안: prepaid_cycles_remaining, next_credit_date, last_credit_date는 시스템 내부에서만 설정 가능
    """
    try:
        user_id = session.get('user_id')
        if not user_id:
            return jsonify({'error': '로그인이 필요합니다.'}), 401

        data = request.get_json(force=True, silent=True) or {}
        product_id = data.get('product_id')
        if not product_id:
            return jsonify({'error': '상품 ID가 필요합니다.'}), 400

        # 보안: 민감한 필드가 요청에 포함되어 있으면 거부
        protected_fields = ['prepaid_cycles_remaining', 'next_credit_date', 'last_credit_date']
        for field in protected_fields:
            if field in data:
                logger.warning(f"보안 경고: 사용자 {user_id}가 보호된 필드 {field} 설정 시도")
                return jsonify({'error': f'필드 {field}는 직접 설정할 수 없습니다.'}), 403

        product = Product.query.get_or_404(product_id)

        start_date = datetime.now()
        if (product.trial_days or 0) > 0:
            trial_end_date = start_date + timedelta(days=product.trial_days)
            end_date = trial_end_date
            status = SubscriptionStatus.TRIAL
        else:
            trial_end_date = None
            end_date = start_date + timedelta(days=30 if product.billing_cycle == 'monthly' else 365)
            status = SubscriptionStatus.ACTIVE

        # 크레딧 관련 필드 계산
        prepaid_cycles_remaining, next_credit_date, last_credit_date = _calculate_subscription_credit_fields(
            start_date, product.billing_cycle
        )

        subscription = Subscription(
            user_id=user_id,
            product_id=product_id,
            status=status,
            start_date=start_date,
            end_date=end_date,
            trial_end_date=trial_end_date,
            auto_renew=False,          # 정기/자동결제 비활성화
            next_billing_date=None,    # 다음 청구일 없음
            prepaid_cycles_remaining=prepaid_cycles_remaining,
            next_credit_date=next_credit_date,
            last_credit_date=last_credit_date
        )
        db.session.add(subscription)
        db.session.commit()

        return jsonify({
            'subscription_id': subscription.id,
            'status': subscription.status.value,
            'end_date': subscription.end_date.isoformat()
        }), 201

    except Exception as e:
        db.session.rollback()
        logger.exception("create_subscription failed")
        return jsonify({'error': str(e)}), 500


def handle_payment_status_change(payment, status, webhook_data):
    """결제 상태 변경 처리"""
    try:
        if status == 'success':
            payment.status = PaymentStatus.COMPLETED
            payment.pg_response_code = 'SUCCESS'
            payment.pg_response_message = '결제 완료'
            
            # 구독 상태 업데이트
            subscription = payment.subscription
            if subscription.status == SubscriptionStatus.TRIAL:
                subscription.status = SubscriptionStatus.ACTIVE
                
                # 구독 활성화 시 크레딧 추가
                try:
                    from src.utils.credit_manager import renew_subscription_credits
                    credit_result = renew_subscription_credits(subscription.user_id, subscription.id)
                    if credit_result['success']:
                        logger.info(f'구독 크레딧 추가 완료: user_id={subscription.user_id}, added={credit_result["added"]}')
                    else:
                        logger.warning(f'구독 크레딧 추가 실패: {credit_result.get("error")}')
                except Exception as e:
                    logger.error(f'구독 크레딧 추가 중 오류 (무시): {str(e)}')
                    # 크레딧 추가 실패해도 구독은 성공한 것으로 처리
                
        elif status == 'cancelled':
            payment.status = PaymentStatus.CANCELLED
            payment.pg_response_code = 'CANCELLED'
            payment.pg_response_message = '결제 취소'
            
            # 구독 상태 업데이트
            subscription = payment.subscription
            # 크레딧 기준 구독자인 경우 남은 크레딧 사용 완료 처리
            try:
                from src.models.user import SubscriptionType
                from src.utils.credit_manager import consume_all_remaining_credits
                
                user = resolve_user_ref(subscription.user_id)
                if user and user.subscription_type == SubscriptionType.CREDIT:
                    credit_result = consume_all_remaining_credits(
                        user_id=subscription.user_id,
                        description='구독 취소로 인한 남은 크레딧 사용 완료',
                        reference_id=str(subscription.id),
                        reference_type='subscription_cancellation'
                    )
                    if credit_result.get('success'):
                        logger.info(f'[handle_payment_status_change] 남은 크레딧 사용 완료: {credit_result.get("consumed", 0)} 크레딧')
                    else:
                        logger.warning(f'[handle_payment_status_change] 남은 크레딧 사용 실패: {credit_result.get("error")}')
            except Exception as e:
                logger.warning(f'[handle_payment_status_change] 크레딧 사용 처리 실패(무시): {str(e)}')
            
            subscription.status = SubscriptionStatus.CANCELLED
            subscription.cancelled_at = datetime.now()
            _reset_subscription_credit_fields(subscription)  # 크레딧 관련 필드 초기화
            
        elif status == 'failed':
            payment.status = PaymentStatus.FAILED
            payment.pg_response_code = 'FAILED'
            payment.pg_response_message = '결제 실패'
            
            # 구독 상태 업데이트
            subscription = payment.subscription
            subscription.status = SubscriptionStatus.EXPIRED
        
        payment.updated_at = datetime.now()
        db.session.commit()
        
    except Exception as e:
        db.session.rollback()
        print(f"결제 상태 변경 처리 실패: {str(e)}")

@subscription_bp.route('/api/subscription/my-subscription', methods=['GET'])
def get_my_subscription():
    """내 구독 정보 조회 - 보안: 사용자는 자신의 구독 정보만 조회 가능"""
    try:
        user_id = session.get('user_id')
        if not user_id:
            return jsonify({'error': '로그인이 필요합니다.'}), 401

        subscription = Subscription.query.filter_by(user_id=user_id, status=SubscriptionStatus.ACTIVE).order_by(
            Subscription.created_at.desc()
        ).first()

        if not subscription:
            return jsonify({'subscription': None}), 200

        # 보안: 사용자 소유권 재확인 (추가 보안 레이어)
        if subscription.user_id != user_id:
            logger.warning(f"보안 경고: 사용자 {user_id}가 다른 사용자의 구독 {subscription.id}에 접근 시도")
            return jsonify({'error': '권한이 없습니다.'}), 403

        return jsonify({
            'subscription': {
                'id': subscription.id,
                'status': subscription.status.value,
                'start_date': subscription.start_date.replace(tzinfo=None).isoformat() + 'Z' if subscription.start_date else None,
                'end_date': subscription.end_date.replace(tzinfo=None).isoformat() + 'Z' if subscription.end_date else None,
                'trial_end_date': subscription.trial_end_date.replace(tzinfo=None).isoformat() + 'Z' if subscription.trial_end_date else None,
                'auto_renew': subscription.auto_renew,
                'next_billing_date': subscription.next_billing_date.replace(tzinfo=None).isoformat() + 'Z' if subscription.next_billing_date else None,
                'payment_method': subscription.payment_method.value if subscription.payment_method else None,
                'payment_id': subscription.payment_id,
                'amount': subscription.amount,
                'billing_cycle': subscription.billing_cycle,
                'prepaid_cycles_remaining': subscription.prepaid_cycles_remaining,
                'next_credit_date': subscription.next_credit_date.isoformat() if subscription.next_credit_date else None,
                'last_credit_date': subscription.last_credit_date.isoformat() if subscription.last_credit_date else None,
                'product': {
                    'id': subscription.product.id,
                    'name': subscription.product.name,
                    'price': subscription.product.price,
                    'billing_cycle': subscription.product.billing_cycle
                }
            }
        }), 200

    except Exception as e:
        logger.exception("get_my_subscription failed")
        return jsonify({'error': str(e)}), 500

# =========================
# 구독 취소 (원타임 기준)
# =========================
@subscription_bp.route('/api/subscription/fix-next-billing-date', methods=['POST'])
def fix_next_billing_date():
    """기존 구독의 next_billing_date 수정 (임시 수정용) - 보안: 사용자 소유권 검증"""
    try:
        user_id = session.get('user_id')
        if not user_id:
            return jsonify({'error': '로그인이 필요합니다.'}), 401

        subscription = Subscription.query.filter_by(
            user_id=user_id, 
            status=SubscriptionStatus.ACTIVE
        ).first()

        if not subscription:
            return jsonify({'error': '활성 구독을 찾을 수 없습니다.'}), 404

        # 보안: 사용자 소유권 재확인
        if subscription.user_id != user_id:
            logger.warning(f"보안 경고: 사용자 {user_id}가 다른 사용자의 구독 {subscription.id} 수정 시도")
            return jsonify({'error': '권한이 없습니다.'}), 403

        # next_billing_date를 end_date와 동일하게 설정
        subscription.next_billing_date = subscription.end_date
        db.session.commit()

        return jsonify({
            'success': True,
            'subscription_id': subscription.id,
            'next_billing_date': subscription.next_billing_date.isoformat(),
            'end_date': subscription.end_date.isoformat()
        }), 200

    except Exception as e:
        db.session.rollback()
        logger.exception("fix_next_billing_date failed")
        return jsonify({'error': str(e)}), 500

@subscription_bp.route('/api/subscription/cancel', methods=['POST'])
def cancel_subscription():
    """구독 취소 — 환불 처리 포함 - 보안: 사용자 소유권 검증"""
    try:
        user_id = session.get('user_id')
        if not user_id:
            return jsonify({'error': '로그인이 필요합니다.'}), 401

        subscription = Subscription.query.filter_by(
            user_id=user_id,
            status=SubscriptionStatus.ACTIVE
        ).first()

        if not subscription:
            return jsonify({'error': '활성 구독이 없습니다.'}), 404

        # 보안: 사용자 소유권 재확인
        if subscription.user_id != user_id:
            logger.warning(f"보안 경고: 사용자 {user_id}가 다른 사용자의 구독 {subscription.id} 취소 시도")
            return jsonify({'error': '권한이 없습니다.'}), 403

        # 환불 처리 (기간 연장형 구독 모델 - 최근 결제부터 환불)
        refund_results = []
        logger.info(f'구독 정보 확인 - payment_method: {subscription.payment_method}, subscription_id: {subscription.id}')
        
        # 🔹 항상 초기화: 어떤 분기에서도 반환 가능하도록
        refund_result = {
            'success': False,
            'error': '환불 처리 로직 미실행',
        }

        # 최근 결제 조회 (가장 최근 완료된 결제)
        latest_payment = None
        try:
            from src.models.payment import PaymentHistory
            latest_payment = PaymentHistory.query.filter_by(
                subscription_id=subscription.id,
                status=PaymentStatus.COMPLETED
            ).order_by(PaymentHistory.created_at.desc()).first()  # 가장 최근 결제만
            
            if latest_payment:
                logger.info(f'최근 결제 발견: payment_key={latest_payment.payment_key}, amount={latest_payment.amount}, created_at={latest_payment.created_at}')
            else:
                logger.warning(f'구독 {subscription.id}의 완료된 결제를 찾을 수 없음')
                
        except Exception as e:
            logger.error(f'최근 결제 조회 중 오류: {str(e)}')
        
        # 기간 연장형 모델: 최근 결제만 환불 처리
        if subscription.payment_method == PaymentMethod.TOSS_PAYMENTS and latest_payment:
            try:
                from src.models.payment import PaymentHistory, PaymentStatus as PHStatus

                # 1) 이 구독의 COMPLETED 결제 전체(최신 → 과거) 조회
                completed_list = (PaymentHistory.query
                                  .filter_by(subscription_id=subscription.id, status=PHStatus.COMPLETED)
                                  .order_by(PaymentHistory.created_at.desc())
                                  .all())

                if not completed_list:
                    logger.info('완료된 결제 없음 — 사용분 정산만 0원으로 처리')
                    usage_meta = {'total_paid': 0, 'total_days': 0, 'used_days': 0, 'usage_charge': 0}
                    usage_payment = None
                else:
                    # 2) 체인 기준 사용분 정산 금액 계산( carryover 금액은 별도 반영하지 않음 )
                    usage_meta = _calc_usage_charge_for_chain(completed_list)
                    usage_charge = usage_meta['usage_charge']
                    logger.info(f'[CancelChain] usage_meta={usage_meta}')

                    # 3) 결제건 전액 환불 (최신→과거)
                    refund_total = 0
                    refund_details = []
                    for ph in completed_list:
                        refund_amount = int(ph.amount or 0)
                        if refund_amount <= 0:
                            continue

                        import uuid, requests
                        refund_no = f'refund_{uuid.uuid4().hex[:16]}'
                        amount_taxable = int(refund_amount / 1.1)
                        amount_vat = refund_amount - amount_taxable

                        refund_data = {
                            'apiKey': os.environ.get('TOSS_API_KEY'),
                            'payToken': ph.payment_key,
                            'refundNo': refund_no,
                            'amount': refund_amount,
                            'amountTaxable': amount_taxable,
                            'amountTaxFree': 0,
                            'amountVat': amount_vat,
                            'amountServiceFee': 0,
                            'reason': '반품 취소(전체 환불)'
                        }
                        r = requests.post(TOSS_CONFIG['refund_url'],
                                          headers={'Content-Type': 'application/json'},
                                          json=refund_data, timeout=30)

                        if r.status_code == 200:
                            refund_total += refund_amount
                            ph.status = PHStatus.REFUNDED
                            ph.description = f'환불 완료 - {refund_no}'
                            db.session.commit()
                            refund_details.append({
                                'payment_id': ph.id,
                                'refund_no': refund_no,
                                'amount': refund_amount
                            })
                        else:
                            logger.error(f'[CancelChain] 환불 실패: ph_id={ph.id}, resp={r.status_code} {r.text}')
                            return jsonify({
                                'success': False,
                                'error': '일부 환불 실패(중단).',
                                'failed_payment_id': ph.id,
                                'raw': r.text
                            }), 400

                    # 4) 사용분 정산 결제 준비 (금액>0일 때만)
                    usage_payment = None
                    if usage_meta['usage_charge'] > 0:
                        usage_payment = _prepare_usage_settlement_payment(
                            user_id=subscription.user_id,
                            subscription=subscription,
                            latest_payment=completed_list[0],  # 아무 PH나 참조 용도(상품/사용자 메타)
                            usage_charge=usage_meta['usage_charge']
                        )

                # 5) 크레딧 기준 구독자인 경우 남은 크레딧 사용 완료 처리
                try:
                    from src.models.user import User, SubscriptionType
                    from src.utils.credit_manager import consume_all_remaining_credits
                    
                    user = resolve_user_ref(subscription.user_id)
                    if user and user.subscription_type == SubscriptionType.CREDIT:
                        credit_result = consume_all_remaining_credits(
                            user_id=subscription.user_id,
                            description='구독 취소로 인한 남은 크레딧 사용 완료',
                            reference_id=str(subscription.id),
                            reference_type='subscription_cancellation'
                        )
                        if credit_result.get('success'):
                            logger.info(f'[CancelChain] 남은 크레딧 사용 완료: {credit_result.get("consumed", 0)} 크레딧')
                        else:
                            logger.warning(f'[CancelChain] 남은 크레딧 사용 실패: {credit_result.get("error")}')
                except Exception as e:
                    logger.warning(f'[CancelChain] 크레딧 사용 처리 실패(무시): {str(e)}')

                # 6) 구독 상태 종료 및 크레딧 관련 필드 초기화
                subscription.status = SubscriptionStatus.CANCELLED
                subscription.next_billing_date = None
                subscription.end_date = datetime.now()
                subscription.cancelled_at = datetime.now()
                _reset_subscription_credit_fields(subscription)  # 크레딧 관련 필드 초기화
                subscription.updated_at = datetime.now()
                db.session.commit()

                # 권한 회수(실패 무시)
                try:
                    from src.models.user import User
                    u = resolve_user_ref(subscription.user_id)
                    if u:
                        u.role = 'role_free'
                        db.session.commit()
                except Exception as e:
                    logger.warning(f'권한 회수 실패(무시): {str(e)}')

                # 6) 응답
                return jsonify({
                    'success': True,
                    'message': '구독이 취소되었고, 전체 환불 후 사용분 정산 결제를 준비했습니다.',
                    'refund': {
                        'mode': 'full_chain',
                        'total_refund': sum(int(ph.amount or 0) for ph in completed_list) if completed_list else 0
                    },
                    'usage_settlement': {
                        'amount': usage_meta.get('usage_charge', 0),
                        'orderId': (usage_payment or {}).get('orderId'),
                        'payUrl': (usage_payment or {}).get('payUrl'),
                        'simulation': (usage_payment or {}).get('simulation'),
                        'meta': {
                            'total_paid': usage_meta.get('total_paid'),
                            'total_days': usage_meta.get('total_days'),
                            'used_days': usage_meta.get('used_days'),
                        }
                    }
                }), 200

            except Exception as e:
                logger.exception('[CancelChain] 전체 환불 + 정산 실패')
                return jsonify({'error': str(e)}), 500
        
        # 구독 상태는 이미 위에서 만료 여부에 따라 설정됨
        
        return jsonify({
            'success': True,
            'message': '구독이 성공적으로 취소되었습니다.',
            'refund': refund_result
        }), 200

    except Exception as e:
        db.session.rollback()
        logger.exception("기간 연장형 구독 취소 실패")
        return jsonify({'error': str(e)}), 500
   

def calculate_refund_amount_for_payment(payment_history):
    """기간 연장형 모델 - 특정 결제의 환불 금액 계산"""
    try:
        if not payment_history.subscription_start_date or not payment_history.subscription_end_date:
            return 0
            
        now = datetime.now()
        start_date = payment_history.subscription_start_date
        end_date = payment_history.subscription_end_date
        
        # 기간 연장형 모델: 추가 결제의 경우 전체 금액 환불
        # 결제 기간이 아직 시작되지 않은 경우 (전액 환불)
        if now < start_date:
            logger.info(f'결제 {payment_history.id} - 기간 미시작으로 전액 환불: {payment_history.amount}원')
            return payment_history.amount
            
        # 결제 기간이 이미 만료된 경우 (환불 불가)
        if now >= end_date:
            logger.info(f'결제 {payment_history.id} - 기간 만료로 환불 불가')
            return 0
            
        # 사용하지 않은 기간 계산
        remaining_days = (end_date - now).days
        total_days = (end_date - start_date).days
        
        if total_days <= 0:
            return 0
            
        # 토스페이먼츠는 부분환불 불가하므로 전체 결제 금액 환불
        # 사용하지 않은 기간과 상관없이 결제된 전체 금액을 환불
        refund_amount = payment_history.amount
        
        logger.info(f'결제 {payment_history.id} 환불 계산 (토스페이먼츠 전체 환불):')
        logger.info(f'  - 결제 금액: {payment_history.amount}원')
        logger.info(f'  - 구독 기간: {start_date} ~ {end_date}')
        logger.info(f'  - 남은 일수: {remaining_days}일 / 총 {total_days}일')
        logger.info(f'  - 환불 금액: {refund_amount}원 (전체 금액 환불)')
        
        return refund_amount
        
    except Exception as e:
        logger.error(f'환불 금액 계산 실패: {str(e)}')
        return 0


# 사용분 정산액 계산 유틸
# 최근 결제 1건을 기준으로 '전체 환불 후 사용분 재결제'에 필요한 금액을 계산.
def _calc_usage_charge(latest_payment) -> dict:
    """
    최근 결제 1건을 기준으로 '전체 환불 후 사용분 재결제'에 필요한 금액을 계산.
    - 전체 환불: latest_payment.amount
    - 사용분 청구: (amount + carryover_credit_amount) * (경과일수 / 총일수) - carryover_credit_amount
      (carryover 크레딧은 먼저 소진된 가치이므로 사용분에서 차감)
    """
    amount = float(latest_payment.amount or 0)
    total_days = int(latest_payment.subscription_days or 0) or (30 if latest_payment.billing_cycle == 'monthly' else 365)

    # 경계 안정화
    start_dt = latest_payment.subscription_start_date or latest_payment.created_at or datetime.now()
    today = datetime.now()
    elapsed_days = max(0, min(total_days, (today.date() - start_dt.date()).days + 1))  # 날짜 기준, 오늘 포함 1일

    # carryover(이월) 금액/일수 (칼럼 없을 수도 있으니 getattr 사용)
    co_amount = float(getattr(latest_payment, 'carryover_credit_amount', 0) or 0)

    # 총 가치(이번 결제액 + 이월금액)에서 사용 비율만큼 사용가치 계산
    used_value = (amount + co_amount) * (elapsed_days / total_days if total_days > 0 else 1.0)

    # 이월금액은 선사용 가치로 본다 → 사용분 청구액 = 사용가치 - 이월금액
    usage_charge = max(0, int(round(used_value - co_amount)))

    return {
        'total_days': total_days,
        'elapsed_days': elapsed_days,
        'carryover_credit_amount': int(co_amount),
        'usage_charge': usage_charge
    }

def _calc_usage_charge_for_chain_monthly(ph_list, as_of=None) -> dict:
    """
    월 기준 구독자용 사용분 정산 금액 계산
    체인 전체 환불 모드 — 구간별 일일단가 반영.
    - 구간별 per-day = (금액 + 이월금액?) / 구간일수
      * 기본은 금액/일수. carryover_credit_amount가 '그 구간의 추가 일수 가치'로 포함된 경우
        (subscription_days에 carryover가 더해진 구조라면) per-day를 (amount + carryover_credit_amount)/subscription_days 로도 선택 가능.
        아래 기본값은 금액/일수이며, 주석의 옵션을 사용하려면 해당 줄을 교체하세요.
    - overlap_days: [구간시작, 구간끝] ∩ [체인시작, as_of] 의 일 수(일 단위, 오늘 포함)
    """
    from datetime import datetime, timedelta, date

    if not ph_list:
        return {'total_paid': 0, 'total_days': 0, 'used_days': 0, 'usage_charge': 0}

    # today 기준 (테스트/재계산 시점 지정 가능)
    now = datetime.now()
    as_of = as_of or now
    as_of_date = as_of.date()

    # COMPLETED만, 정산건 제외
    from src.models.payment import PaymentStatus as PHStatus
    def _is_effective(ph): 
        status_ok = (getattr(ph, 'status', None) == PHStatus.COMPLETED)
        oid = str(getattr(ph, 'order_id', '') or '')
        cyc = str(getattr(ph, 'billing_cycle', '') or '').lower()
        pname = str(getattr(ph, 'product_name', '') or '')
        is_settlement = oid.startswith('mlink_usage_') or cyc == 'onetime' or ('정산' in pname)
        return status_ok and not is_settlement

    eff = [ph for ph in ph_list if _is_effective(ph)]
    if not eff:
        return {'total_paid': 0, 'total_days': 0, 'used_days': 0, 'usage_charge': 0}

    # 체인 시작일 = 포함 구간들의 최소 시작일
    def _start_of(ph):
        return (ph.subscription_start_date or ph.created_at or now)

    def _days_of(ph):
        d = int(getattr(ph, 'subscription_days', 0) or 0)
        if d <= 0:
            bc = str(getattr(ph, 'billing_cycle', 'monthly')).lower()
            d = 30 if bc == 'monthly' else 365
        return d

    chain_start_dt = min(_start_of(ph) for ph in eff)
    chain_start_date = chain_start_dt.date()

    total_paid = 0
    total_days = 0
    used_days_total = 0
    used_value = 0.0

    for ph in eff:
        amount = float(getattr(ph, 'amount', 0) or 0)
        total_paid += amount

        # 구간 설정
        start_dt = _start_of(ph)
        days = _days_of(ph)
        total_days += days

        end_dt = getattr(ph, 'subscription_end_date', None)
        if not end_dt:
            end_dt = start_dt + timedelta(days=days)  # 안전한 백업

        start_d = start_dt.date()
        end_d   = end_dt.date()

        # as_of 보다 미래면 일부만, 과거면 전부, 아직 시작 전이면 0
        left  = max(chain_start_date, start_d)
        right = min(as_of_date, end_d)

        overlap_days = (right - left).days + 1
        if overlap_days < 0:
            overlap_days = 0

        # ⚖ per-day 계산
        # 기본: 결제 금액 / 구간일수
        per_day = amount / days if days > 0 else amount
        # 만약 carryover_credit_amount를 구간 가치에 포함시키려면(구간일수에 carryover가 더해진 구조):
        # co_amt = float(getattr(ph, 'carryover_credit_amount', 0) or 0)
        # per_day = (amount + co_amt) / days if days > 0 else (amount + co_amt)

        # 사용 가치 합산
        used_value += per_day * max(0, min(overlap_days, days))
        used_days_total += max(0, min(overlap_days, days))

    usage_charge = int(round(used_value))

    return {
        'total_paid': int(total_paid),
        'total_days': int(total_days),
        'used_days': int(used_days_total),
        'usage_charge': int(usage_charge),
        'chain_start': chain_start_dt
    }


def _calc_usage_charge_for_chain_credit(user_id, ph_list) -> dict:
    """
    크레딧 기준 구독자용 사용분 정산 금액 계산
    - credit_balances의 남은 credit을 기준으로 사용한 크레딧 계산
    - usage_charge = (monthly_credits - 남은 크레딧) × 크레딧당 단가
    """
    from src.models.credit import CreditBalance

    if not ph_list:
        return {'total_paid': 0, 'total_days': 0, 'used_days': 0, 'usage_charge': 0}

    # 크레딧 잔액 조회
    credit_balance = CreditBalance.query.filter_by(user_id=user_id).first()
    if not credit_balance:
        return {'total_paid': 0, 'total_days': 0, 'used_days': 0, 'usage_charge': 0}

    # 플랜별 단가 계산 (subscription의 product.price 사용)
    subscription = Subscription.query.filter_by(
        user_id=user_id,
        status=SubscriptionStatus.ACTIVE
    ).first()

    if not subscription or not subscription.product:
        # 구독 정보가 없으면 0 반환
        total_paid = sum(float(getattr(ph, 'amount', 0) or 0) for ph in ph_list)
        return {
            'total_paid': int(total_paid),
            'total_days': 0,
            'used_days': 0,
            'usage_charge': 0,
            'chain_start': None
        }

    # 남은 크레딧
    remaining_credit = credit_balance.subscription_credit

    # 플랜별 단가 (크레딧당 가격)
    # product.price를 월간 크레딧으로 나눔
    product = subscription.product
    product_name = product.name.lower()

    # 플랜별 월간 크레딧
    if 'basic' in product_name:
        monthly_credits = 1900
    elif 'plus' in product_name:
        monthly_credits = 3900
    elif 'pro' in product_name:
        monthly_credits = 19000
    else:
        monthly_credits = 1900  # 기본값

    # 크레딧당 단가 = product.price / monthly_credits
    price_per_credit = product.price / monthly_credits if monthly_credits > 0 else 0

    # 사용분 정산 금액 = (월간 크레딧 - 남은 크레딧) × 크레딧당 단가
    used_credits = monthly_credits - remaining_credit
    usage_charge = int(round(used_credits * price_per_credit))

    # total_paid는 ph_list의 총액
    total_paid = sum(float(getattr(ph, 'amount', 0) or 0) for ph in ph_list)

    return {
        'total_paid': int(total_paid),
        'total_days': 0,  # 크레딧 기준은 일수 계산 불필요
        'used_days': 0,   # 크레딧 기준은 일수 계산 불필요
        'usage_charge': usage_charge,
        'chain_start': None
    }


def _calc_usage_charge_for_chain(ph_list, as_of=None) -> dict:
    """
    체인 전체 환불 모드 — 구독 타입에 따라 분기 처리
    - user.subscription_type == 'MONTHLY': 월 기준 계산 (_calc_usage_charge_for_chain_monthly)
    - user.subscription_type == 'CREDIT': 크레딧 기준 계산 (_calc_usage_charge_for_chain_credit)
    """
    from src.models.user import SubscriptionType

    if not ph_list:
        return {'total_paid': 0, 'total_days': 0, 'used_days': 0, 'usage_charge': 0}

    # user_id 추출 (ph_list의 첫 번째 항목에서)
    user_id = getattr(ph_list[0], 'user_id', None)
    if not user_id:
        # user_id가 없으면 월 기준 로직 사용
        return _calc_usage_charge_for_chain_monthly(ph_list, as_of)

    # 사용자 정보 조회
    user = resolve_user_ref(user_id)
    if not user:
        # 사용자 정보가 없으면 월 기준 로직 사용
        return _calc_usage_charge_for_chain_monthly(ph_list, as_of)

    # 구독 타입에 따라 분기
    if user.subscription_type == SubscriptionType.CREDIT:
        # 크레딧 기준 구독자
        return _calc_usage_charge_for_chain_credit(user_id, ph_list)
    else:
        # 월 기준 구독자 (기본값)
        return _calc_usage_charge_for_chain_monthly(ph_list, as_of)


@subscription_bp.route('/api/subscription/payment-history', methods=['GET'])
def get_payment_history():
    """사용자의 결제 이력 조회"""
    try:
        user_id = session.get('user_id')
        if not user_id:
            return jsonify({'error': '로그인이 필요합니다.'}), 401

        # 사용자의 모든 결제 이력 조회 (최신순) - PENDING 상태 제외
        payments = PaymentHistory.query.filter_by(user_id=user_id).filter(
            PaymentHistory.status != PaymentStatus.PENDING
        ).order_by(PaymentHistory.created_at.desc()).all()
        
        # PaymentHistory 객체를 딕셔너리로 변환
        payments_data = []
        for payment in payments:
            payment_dict = payment.to_dict()
            payments_data.append(payment_dict)
        
        logger.info(f'사용자 {user_id}의 결제 이력 조회: {len(payments_data)}건')
        
        return jsonify({
            'success': True,
            'payments': payments_data
        }), 200
        
    except Exception as e:
        logger.error(f'결제 이력 조회 실패: {str(e)}')
        return jsonify({'error': '결제 이력 조회에 실패했습니다.'}), 500


# --- 추가 유틸: 모든 잔여 기간(미래 구간) 환불 + 구독 즉시 해지 ---
def _refund_all_future_periods_and_cancel(subscription):
    """
    현재 활성 구독의 '지금 이후 구간'에 해당하는 결제들을 최근 순서로 전액 환불하고,
    각 환불마다 subscription.end_date에서 subscription_days를 차감(회수)한다.
    end_date <= now가 되면 구독을 CANCELLED 처리한다.

    반환:
      {
        "total_refund": int,
        "items": [
          {"payment_history_id": int, "amount": int, "refund_no": str, "success": bool, "error": str|None}
        ]
      }
    """
    from src.models.payment import PaymentHistory
    from src.models.subscription import PaymentStatus, SubscriptionStatus
    import uuid, requests, os

    now = datetime.now()
    details = []
    total_refund = 0

    # 활성 구독의 PaymentHistory 중 완료건만 최신순
    histories = PaymentHistory.query.filter_by(
        subscription_id=subscription.id,
        status=PaymentStatus.COMPLETED
    ).order_by(PaymentHistory.created_at.desc()).all()

    # 더 이상 미래 구간이 없으면 바로 취소 처리
    if not histories or subscription.end_date <= now:
        # 크레딧 기준 구독자인 경우 남은 크레딧 사용 완료 처리
        try:
            from src.models.user import User, SubscriptionType
            from src.utils.credit_manager import consume_all_remaining_credits
            
            user = resolve_user_ref(subscription.user_id)
            if user and user.subscription_type == SubscriptionType.CREDIT:
                credit_result = consume_all_remaining_credits(
                    user_id=subscription.user_id,
                    description='구독 취소로 인한 남은 크레딧 사용 완료',
                    reference_id=str(subscription.id),
                    reference_type='subscription_cancellation'
                )
                if credit_result.get('success'):
                    logger.info(f'[_refund_all_future_periods_and_cancel] 남은 크레딧 사용 완료: {credit_result.get("consumed", 0)} 크레딧')
                else:
                    logger.warning(f'[_refund_all_future_periods_and_cancel] 남은 크레딧 사용 실패: {credit_result.get("error")}')
        except Exception as e:
            logger.warning(f'[_refund_all_future_periods_and_cancel] 크레딧 사용 처리 실패(무시): {str(e)}')
        
        subscription.status = SubscriptionStatus.CANCELLED
        subscription.next_billing_date = None
        subscription.cancelled_at = datetime.now()
        _reset_subscription_credit_fields(subscription)  # 크레딧 관련 필드 초기화
        subscription.updated_at = datetime.now()
        db.session.commit()
        return {"total_refund": 0, "items": []}

    for ph in histories:
        # 이 결제가 만든 구간이 이미 모두 지난 경우 → 스킵
        if not ph.subscription_start_date or not ph.subscription_end_date:
            continue
        if ph.subscription_end_date <= now:
            continue  # 이미 지난 구간

        # 이 결제의 환불은 "전액 환불"로 처리 (현재 코드베이스 정책과 동일)
        amount = ph.amount
        refund_no = f'refund_{uuid.uuid4().hex[:16]}'

        # 과세/부가세 계산
        taxable = int(amount / 1.1)
        vat = amount - taxable

        refund_data = {
            'apiKey': os.environ.get('TOSS_API_KEY'),
            'payToken': ph.payment_key,
            'refundNo': refund_no,
            'amount': amount,
            'amountTaxable': taxable,
            'amountTaxFree': 0,
            'amountVat': vat,
            'amountServiceFee': 0,
            'reason': '반품 취소 환불'
        }

        try:
            r = requests.post(
                TOSS_CONFIG['refund_url'],
                headers={'Content-Type': 'application/json'},
                json=refund_data,
                timeout=30
            )
            if r.status_code == 200:
                # 환불 성공 → PaymentHistory 상태 변경 및 기간 회수
                ph.status = PaymentStatus.REFUNDED
                ph.description = f'플랜 변경 환불 완료 - {refund_no}'
                db.session.commit()

                # 해당 결제가 부여한 기간 회수
                if ph.subscription_days:
                    subscription.end_date = subscription.end_date - timedelta(days=ph.subscription_days)
                    subscription.updated_at = datetime.now()
                    db.session.commit()

                total_refund += amount
                details.append({"payment_history_id": ph.id, "amount": amount, "refund_no": refund_no, "success": True, "error": None})

                # 더 회수했더니 이미 현재 시점 이전이라면 루프 종료
                if subscription.end_date <= now:
                    break
            else:
                # 환불 실패 → 기록만 남기고 계속 진행(다음 PH도 시도)
                details.append({"payment_history_id": ph.id, "amount": amount, "refund_no": refund_no, "success": False, "error": r.text})
        except Exception as e:
            details.append({"payment_history_id": ph.id, "amount": amount, "refund_no": refund_no, "success": False, "error": str(e)})

    # 최종 상태 정리
    if subscription.end_date <= now:
        # 크레딧 기준 구독자인 경우 남은 크레딧 사용 완료 처리
        try:
            from src.models.user import SubscriptionType
            from src.utils.credit_manager import consume_all_remaining_credits
            
            user = resolve_user_ref(subscription.user_id)
            if user and user.subscription_type == SubscriptionType.CREDIT:
                credit_result = consume_all_remaining_credits(
                    user_id=subscription.user_id,
                    description='구독 취소로 인한 남은 크레딧 사용 완료',
                    reference_id=str(subscription.id),
                    reference_type='subscription_cancellation'
                )
                if credit_result.get('success'):
                    logger.info(f'[_refund_all_future_periods_and_cancel] 남은 크레딧 사용 완료: {credit_result.get("consumed", 0)} 크레딧')
                else:
                    logger.warning(f'[_refund_all_future_periods_and_cancel] 남은 크레딧 사용 실패: {credit_result.get("error")}')
        except Exception as e:
            logger.warning(f'[_refund_all_future_periods_and_cancel] 크레딧 사용 처리 실패(무시): {str(e)}')
        
        subscription.status = SubscriptionStatus.CANCELLED
        subscription.next_billing_date = None
        subscription.cancelled_at = datetime.now()
        _reset_subscription_credit_fields(subscription)  # 크레딧 관련 필드 초기화
        db.session.commit()

        # 권한 회수
        try:
            user = resolve_user_ref(subscription.user_id)
            if user:
                user.role = 'role_free'
                db.session.commit()
        except Exception as e:
            logger.error(f'권한 회수 실패: {str(e)}')

    return {"total_refund": total_refund, "items": details}

def _prepare_toss_payment_for_product(user_id, product: Product, carryover: dict | None = None):
    """
    새 상품 결제를 위한 Toss 결제 세션 생성 유틸.
    - PaymentHistory(PENDING) 임시 레코드 생성
    - payUrl/checkoutPage 반환
    """
    import uuid, requests
    from datetime import datetime

    amount = int(product.price)
    product_name = product.name
    billing_cycle = product.billing_cycle

    # order_id 구성: mlink_userId_random_amount_billingCycle_timestamp
    order_id = f"mlink_{user_id}_{uuid.uuid4().hex[:8]}_{amount}_{billing_cycle}_{int(datetime.now().timestamp())}"

    # 부가세 계산
    amount_taxable = int(amount / 1.1)
    amount_vat = amount - amount_taxable

    payment_data = {
        'orderNo': order_id,
        'amount': amount,
        'amountTaxFree': 0,
        'amountTaxable': amount_taxable,
        'amountVat': amount_vat,
        'productDesc': product_name,
        'apiKey': TOSS_CONFIG['api_key'],
        'autoExecute': True,
        'resultCallback': 'https://pay.toss.im/payfront/demo/callback',
        'callbackVersion': 'V2',
        'retUrl': f'{BACK_URL}/api/v2/payments/toss/success?orderno={order_id}&status=PAY_COMPLETE',
        'retCancelUrl': f'{FRONT_URL}/payment/fail?code=PAY_PROCESS_CANCELED&message=결제가 취소되었습니다&orderId={order_id}',
    }

    r = requests.post(
        TOSS_CONFIG['base_url'],
        headers={'Content-Type': 'application/json'},
        json=payment_data,
        timeout=10
    )

    if r.status_code == 200:
        result = r.json()
        pay_token = result.get('payToken')

        # 임시 PaymentHistory 생성 (PENDING)
        temp_ph = PaymentHistory(
            user_id=user_id,
            subscription_id=None,
            payment_key=pay_token,
            order_id=order_id,
            amount=amount,
            status=PaymentStatus.PENDING,
            payment_method=PaymentMethod.TOSS_PAYMENTS,
            product_id=product.id,
            product_name=product_name,
            product_price=amount,
            billing_cycle=billing_cycle,
            # ⬇⬇ 캐리오버 저장
            carryover_credit_amount=(carryover or {}).get('credit_amount', 0),
            carryover_credit_days=(carryover or {}).get('credit_days', 0),
            created_at=datetime.now()
        )
        db.session.add(temp_ph)
        db.session.commit()

        return {
            "paymentKey": pay_token,
            "orderId": order_id,
            "amount": amount,
            "orderName": product_name,
            "payUrl": result.get('checkoutPage', ''),
            "simulation": False
        }

    # 실패 시 시뮬레이션 리턴(기존 prepare와 통일)
    return {
        "paymentKey": f'sim_payment_key_{order_id}',
        "orderId": order_id,
        "amount": amount,
        "orderName": product_name,
        "payUrl": "",
        "simulation": True
    }

@subscription_bp.route('/api/subscription/change-plan', methods=['POST'])
def change_plan():
    """
    req: { "new_product_id": int, "strategy": "carryover"|"refund_all" }
    보안: 사용자 소유권 검증
    """
    try:
        user_id = session.get('user_id')
        if not user_id:
            return jsonify({'error': '로그인이 필요합니다.'}), 401

        data = request.get_json(force=True, silent=True) or {}
        new_product_id = data.get('new_product_id')
        strategy = (data.get('strategy') or 'carryover').lower()

        if not new_product_id:
            return jsonify({'error': 'new_product_id가 필요합니다.'}), 400

        product = Product.query.get_or_404(new_product_id)

        # 현재 활성 구독 조회
        subscription = Subscription.query.filter_by(
            user_id=user_id, status=SubscriptionStatus.ACTIVE
        ).first()

        # 기존 구독이 있는 경우 소유권 검증
        if subscription and subscription.user_id != user_id:
            logger.warning(f"보안 경고: 사용자 {user_id}가 다른 사용자의 구독 {subscription.id} 변경 시도")
            return jsonify({'error': '권한이 없습니다.'}), 403

        # 기존 구독 없으면 곧장 새 결제 준비
        if not subscription:
            payment_prep = _prepare_toss_payment_for_product(user_id, product, carryover=None)
            return jsonify({
                'success': True,
                'refund': {'total_refund': 0, 'items': []},
                'carryover': {'credit_amount': 0, 'credit_days': 0},
                'payment': payment_prep
            }), 200

        # 전략 분기
        if strategy == 'refund_all':
            # 기존 동작: 지금 이후 전액 환불 + 취소
            refund_result = _refund_all_future_periods_and_cancel(subscription)
            # 캐리오버 없음
            carryover = {'credit_amount': 0, 'credit_days': 0}
            payment_prep = _prepare_toss_payment_for_product(user_id, product, carryover=None)

            return jsonify({
                'success': True,
                'refund': refund_result,
                'carryover': carryover,
                'payment': payment_prep
            }), 200

        # 기본: carryover
        # 1) 순수 미래 구간만 환불(현재 구간은 환불하지 않음)
        refund_result = _refund_pure_future_periods(subscription)

        # 2) 현재 구간 남은 가치를 새 상품 일수로 환산
        carryover = _compute_partial_credit_for_current_period(subscription, product)

        # 3) 새 상품 결제 준비 (PENDING PH에 carryover 저장)
        payment_prep = _prepare_toss_payment_for_product(user_id, product, carryover=carryover)

        return jsonify({
            'success': True,
            'refund': refund_result,
            'carryover': carryover,
            'payment': payment_prep
        }), 200

    except Exception as e:
        logger.exception("change_plan failed")
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

# 🔵 NEW: 현재 구간의 남은 가치를 새 상품 일수로 환산
def _compute_partial_credit_for_current_period(subscription, new_product):
    """
    현재(오늘 포함) 활성 구간 1건의 남은 가치를 새 상품 기준 '일수'로 환산
    - 현재 코드베이스 정책: 토스 부분환불 불가 → 금액은 환불하지 않고 '일수'로 업사이클
    """
    from src.models.payment import PaymentHistory
    from src.models.subscription import PaymentStatus
    from sqlalchemy import func
    today = datetime.now().date()
    
    try:
        cur = PaymentHistory.query.filter(
            PaymentHistory.subscription_id == subscription.id,
            PaymentHistory.status == PaymentStatus.COMPLETED,
            PaymentHistory.subscription_start_date <= func.now(),
            PaymentHistory.subscription_end_date >= func.now()
        ).order_by(PaymentHistory.created_at.desc()).first()

        if not cur or not cur.subscription_days or cur.subscription_days <= 0:
            return {"credit_amount": 0, "credit_days": 0}

        total_days = int(cur.subscription_days)
        remaining_days = max(0, (cur.subscription_end_date.date() - today).days)
        if remaining_days <= 0:
            return {"credit_amount": 0, "credit_days": 0}

        # 현재 구간 금액 대비 남은 일수 비례 금액
        credit_amount = int(cur.amount * (remaining_days / total_days))

        # 새 상품 일수(월 30, 년 365) 기준 환산
        new_cycle_days = 365 if (getattr(new_product, 'billing_cycle', 'monthly') == 'yearly') else 30
        price_per_day_new = new_product.price / new_cycle_days
        credit_days = int(credit_amount / price_per_day_new)

        current_app.logger.info(
            f"[ChangePlan] carryover compute: remain={remaining_days}/{total_days}, "
            f"amount={cur.amount} -> credit_amount={credit_amount}, credit_days={credit_days}"
        )
        
        result = {"credit_amount": credit_amount, "credit_days": credit_days}
        current_app.logger.info(f"[ChangePlan] _compute_partial_credit_for_current_period return: {result}")
        return result
    except Exception as e:
        current_app.logger.error(f"[ChangePlan] _compute_partial_credit_for_current_period failed: {str(e)}")
        current_app.logger.error(f"[ChangePlan] subscription_id: {subscription.id if subscription else 'None'}")
        current_app.logger.error(f"[ChangePlan] new_product_id: {new_product.id if new_product else 'None'}")
        # 예외 발생 시 안전한 기본값 반환
        return {"credit_amount": 0, "credit_days": 0}

def _refund_pure_future_periods(subscription):
    """
    '지금 이후'에 완전히 위치하는 결제 기간(아직 시작도 안 한 구간)만 전액 환불.
    현재 진행 중인 구간(now ∈ [start, end])은 환불하지 않음.
    """
    from src.models.payment import PaymentHistory
    from src.models.subscription import PaymentStatus, SubscriptionStatus

    now = datetime.now()
    details = []
    total_refund = 0

    histories = PaymentHistory.query.filter_by(
        subscription_id=subscription.id,
        status=PaymentStatus.COMPLETED
    ).order_by(PaymentHistory.created_at.desc()).all()

    for ph in histories:
        if not ph.subscription_start_date or not ph.subscription_end_date:
            continue
        # 완전한 미래 구간만 선택
        if ph.subscription_start_date > now:
            amount = ph.amount
            refund_no = f'refund_{uuid.uuid4().hex[:16]}'
            taxable = int(amount / 1.1)
            vat = amount - taxable

            refund_data = {
                'apiKey': os.environ.get('TOSS_API_KEY'),
                'payToken': ph.payment_key,
                'refundNo': refund_no,
                'amount': amount,
                'amountTaxable': taxable,
                'amountTaxFree': 0,
                'amountVat': vat,
                'amountServiceFee': 0,
                'reason': '플랜 변경(미래 구간 환불)'
            }

            try:
                r = requests.post(TOSS_CONFIG['refund_url'],
                                  headers={'Content-Type': 'application/json'},
                                  json=refund_data, timeout=30)
                if r.status_code == 200:
                    ph.status = PaymentStatus.REFUNDED
                    ph.description = f'미래구간 환불 완료 - {refund_no}'
                    db.session.commit()

                    # 이 결제가 부여한 기간만큼 end_date 회수
                    if ph.subscription_days:
                        subscription.end_date = subscription.end_date - timedelta(days=ph.subscription_days)
                        subscription.updated_at = datetime.now()
                        db.session.commit()

                    total_refund += amount
                    details.append({"payment_history_id": ph.id, "amount": amount,
                                    "refund_no": refund_no, "success": True, "error": None})
                else:
                    details.append({"payment_history_id": ph.id, "amount": amount,
                                    "refund_no": refund_no, "success": False, "error": r.text})
            except Exception as e:
                details.append({"payment_history_id": ph.id, "amount": amount,
                                "refund_no": refund_no, "success": False, "error": str(e)})

    return {"total_refund": total_refund, "items": details}        


# 사용분 정산 결제 준비
def _prepare_usage_settlement_payment(user_id, subscription, latest_payment, usage_charge: int):
    """
    사용분 정산 결제를 위해 토스 '결제 준비'를 생성.
    - product_name: '구독 사용분 정산'
    - billing_cycle: 'onetime'로 표기하되, DB에는 monthly처럼 저장해도 무방(실제 갱신 없음)
    - PaymentHistory: PENDING 생성
    """
    if usage_charge <= 0:
        return None

    import uuid, requests
    order_id = f"mlink_usage_{user_id}_{uuid.uuid4().hex[:8]}_{usage_charge}_onetime_{int(datetime.now().timestamp())}"

    product_name = '반품 정산'
    headers = {'Content-Type': 'application/json'}

    # 세금 내역(부가세 10%)
    taxable = int(usage_charge / 1.1)
    vat = usage_charge - taxable

    payload = {
        'orderNo': order_id,
        'amount': usage_charge,
        'amountTaxFree': 0,
        'amountTaxable': taxable,
        'amountVat': vat,
        'productDesc': product_name,
        'apiKey': TOSS_CONFIG['api_key'],
        'autoExecute': True,
        'resultCallback': 'https://pay.toss.im/payfront/demo/callback',
        'callbackVersion': 'V2',
        'retUrl': f'{BACK_URL}/api/v2/payments/toss/success?orderno={order_id}&status=PAY_COMPLETE',
        'retCancelUrl': f'{FRONT_URL}/payment/fail?code=PAY_PROCESS_CANCELED&message=정산 결제가 취소되었습니다&orderId={order_id}'
    }

    try:
        resp = requests.post(TOSS_CONFIG['base_url'], headers=headers, json=payload, timeout=10)
        current_app.logger.info(f'[UsageSettle] toss prepare resp {resp.status_code} {resp.text}')
        pay_token = None
        checkout = ''
        simulation = False

        if resp.status_code == 200:
            data = resp.json()
            pay_token = data.get('payToken')
            checkout = data.get('checkoutPage', '')
            current_app.logger.info(f'[UsageSettle] 토스 결제 준비 성공: pay_token={pay_token}, checkout={checkout}')
        else:
            # 시뮬레이션 fallback
            current_app.logger.warning(f'[UsageSettle] 토스 결제 준비 실패: {resp.status_code} - {resp.text}')
            pay_token = f'sim_usage_{order_id}'
            checkout = ''  # 빈 문자열로 설정하여 프론트에서 감지할 수 있도록
            simulation = True

        # PENDING PH 생성(정산건)
        from src.models.payment import PaymentHistory, PaymentStatus as PHStatus
        ph = PaymentHistory(
            user_id=user_id,
            subscription_id=subscription.id if subscription else None,
            payment_key=pay_token,
            order_id=order_id,
            amount=usage_charge,
            status=PHStatus.PENDING,
            payment_method='toss_payments',
            product_id=getattr(latest_payment, 'product_id', None),
            product_name=product_name,
            product_price=usage_charge,
            billing_cycle='onetime',
            # 정산 결제는 구독일수 개념 없음
            subscription_days=None,
            subscription_start_date=None,
            subscription_end_date=None,
            created_at=datetime.now()
        )
        db.session.add(ph)
        db.session.commit()

        return {
            'orderId': order_id,
            'paymentKey': pay_token,
            'amount': usage_charge,
            'payUrl': checkout,
            'simulation': simulation
        }

    except Exception as e:
        current_app.logger.error(f'[UsageSettle] prepare failed: {str(e)}')
        db.session.rollback()
        return None


@subscription_bp.route('/api/subscription/cancel-orchestrated', methods=['POST'])
def cancel_subscription_orchestrated():
    """
    "정산 먼저 → 전체 환불" 모드 시작점.
    1) 사용분 정산 금액 계산
    2) 정산 결제 준비 + payUrl 반환
    보안: 사용자 소유권 검증
    """
    try:
        user_id = session.get('user_id')
        if not user_id:
            return jsonify({'success': False, 'error': '로그인이 필요합니다.'}), 401

        subscription = Subscription.query.filter_by(
            user_id=user_id, status=SubscriptionStatus.ACTIVE
        ).first()
        if not subscription:
            return jsonify({'success': False, 'error': '활성 구독이 없습니다.'}), 404

        # 보안: 사용자 소유권 재확인
        if subscription.user_id != user_id:
            logger.warning(f"보안 경고: 사용자 {user_id}가 다른 사용자의 구독 {subscription.id} 취소 시도")
            return jsonify({'success': False, 'error': '권한이 없습니다.'}), 403

        logger.info(f'구독 정보 확인 - payment_method: {subscription.payment_method}, subscription_id: {subscription.id}')

        # 최근 결제 조회 (가장 최근 완료된 결제)  ✅ PHStatus 사용
        latest_payment = None
        try:
            latest_payment = (PaymentHistory.query
                              .filter_by(subscription_id=subscription.id, status=PHStatus.COMPLETED)
                              .order_by(PaymentHistory.created_at.desc())
                              .first())
            if latest_payment:
                logger.info(f'최근 결제 발견: payment_key={latest_payment.payment_key}, amount={latest_payment.amount}, created_at={latest_payment.created_at}')
            else:
                logger.warning(f'구독 {subscription.id}의 완료된 결제를 찾을 수 없음')
        except Exception as e:
            logger.error(f'최근 결제 조회 중 오류: {str(e)}')

        # Toss 모델 + 최근 결제가 있어야 정산 흐름 가능
        if subscription.payment_method == PaymentMethod.TOSS_PAYMENTS and latest_payment:
            try:
                # 이 구독의 COMPLETED 결제 전체(최신 → 과거) 조회  ✅ PHStatus 사용
                completed_list = (PaymentHistory.query
                                  .filter_by(subscription_id=subscription.id, status=PHStatus.COMPLETED)
                                  .order_by(PaymentHistory.created_at.desc())
                                  .all())

                if not completed_list:
                    logger.info('완료된 결제 없음 — 사용분 정산 0원으로 처리')
                    return jsonify({
                        'success': True,
                        'usage_settlement': {
                            'amount': 0,
                            'orderId': None,
                            'payUrl': None,
                            'simulation': False
                        },
                        'message': '정산할 금액이 없어 바로 취소/환불을 진행할 수 있습니다.'
                    }), 200

                # 1) 사용분 정산 금액 계산
                usage_meta = _calc_usage_charge_for_chain(completed_list)
                usage_charge = int(usage_meta.get('usage_charge', 0))
                logger.info(f'[CancelChain] usage_meta={usage_meta}')

                if usage_charge <= 0:
                    # 정산 0원이면 바로 다음 단계(전체 환불 플로우 트리거)를 프론트에서 진행하게 함
                    return jsonify({
                        'success': True,
                        'usage_settlement': {
                            'amount': 0,
                            'orderId': None,
                            'payUrl': None,
                            'simulation': False,
                            'meta': {
                                'total_paid': usage_meta.get('total_paid', 0),
                                'total_days': usage_meta.get('total_days', 0),
                                'used_days': usage_meta.get('used_days', 0)
                            }
                        },
                        'message': '정산 금액이 0원입니다. 바로 전체 환불/취소를 진행하세요.'
                    }), 200

                # 2) 정산 결제 준비  ✅ 실패 시 None 처리 방어
                usage_payment = _prepare_usage_settlement_payment(
                    user_id=subscription.user_id,
                    subscription=subscription,
                    latest_payment=completed_list[0],  # 메타 참조용
                    usage_charge=usage_charge
                )
                if not usage_payment or not usage_payment.get('orderId') or not usage_payment.get('payUrl') or usage_payment.get('payUrl', '').strip() == '':
                    return jsonify({
                        'success': False,
                        'error': '정산 결제 링크 생성 실패',
                        'detail': usage_payment or {}
                    }), 400

                # 3) payUrl/orderId 반환 (프론트가 결제 → 콜백/웹훅에서 finish_settlement_then_refund 실행)
                return jsonify({
                    'success': True,
                    'usage_settlement': {
                        'amount': usage_charge,
                        'orderId': usage_payment['orderId'],
                        'payUrl': usage_payment['payUrl'],
                        'simulation': usage_payment.get('simulation', False),
                        'meta': {
                            'total_paid': usage_meta.get('total_paid'),
                            'total_days': usage_meta.get('total_days'),
                            'used_days': usage_meta.get('used_days'),
                        }
                    },
                    'message': '정산 결제부터 진행해주세요. 결제 완료 후 전체 환불/취소가 이어집니다.'
                }), 200

            except Exception as e:
                logger.exception('[CancelChain] 정산 링크 생성 실패')
                return jsonify({'success': False, 'error': str(e)}), 500

        # Toss가 아니거나 latest_payment 없음 → 오케스트레이션 불가 케이스(기본 메시지)
        return jsonify({
            'success': False,
            'error': '오케스트레이션 취소를 진행할 수 없습니다. (PG 또는 결제 이력 확인 필요)'
        }), 400

    except Exception as e:
        db.session.rollback()
        logger.exception("기간 연장형 구독 취소(오케스트레이션) 실패")
        return jsonify({'success': False, 'error': str(e)}), 500

# =========================
# 사용자 프로필
# =========================
@subscription_bp.route('/api/user/profile', methods=['GET'])
def get_user_profile():
    """사용자 프로필 조회"""
    try:
        user_id = session.get('user_id')
        if not user_id:
            return jsonify({'error': '로그인이 필요합니다.'}), 401
        
        profile = UserProfile.query.filter_by(user_id=user_id).first()
        
        if not profile:
            return jsonify({
                'company_name': '',
                'business_number': '',
                'phone': '',
                'address': '',
                'department': '',
                'position': ''
            }), 200
        
        return jsonify({
            'company_name': profile.company_name or '',
            'business_number': profile.business_number or '',
            'phone': profile.phone or '',
            'address': profile.address or '',
            'department': profile.department or '',
            'position': profile.position or ''
        }), 200
        
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@subscription_bp.route('/api/user/profile', methods=['PUT'])
def update_user_profile():
    """사용자 프로필 업데이트"""
    try:
        user_id = session.get('user_id')
        if not user_id:
            return jsonify({'error': '로그인이 필요합니다.'}), 401
        
        data = request.get_json()
        
        profile = UserProfile.query.filter_by(user_id=user_id).first()
        
        if not profile:
            profile = UserProfile(user_id=user_id)
            db.session.add(profile)
        
        profile.company_name = data.get('company_name', '')
        profile.business_number = data.get('business_number', '')
        profile.phone = data.get('phone', '')
        profile.address = data.get('address', '')
        profile.department = data.get('department', '')
        profile.position = data.get('position', '')
        profile.updated_at = datetime.now(timezone.utc)
        
        db.session.commit()
        
        return jsonify({'message': '프로필이 업데이트되었습니다.'}), 200
        
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500

@subscription_bp.route('/api/subscription/finish-settlement-then-refund', methods=['POST'])
def finish_settlement_then_refund():
    """
    1) 프론트에서 정산 결제(usage_settlement)를 완료한 뒤 호출되는 마무리 단계
    2) (있다면) 정산 결제의 Toss 상태를 확인해 PENDING -> COMPLETED 로 확정
    3) 체인에 속한 COMPLETED 결제들을 최신→과거 순으로 '전액 환불'
    4) 구독을 CANCELLED 처리, 권한 회수
    응답: { success, refund: { total_refund, items: [...] } }
    보안: 사용자 소유권 검증
    """
    try:
        user_id = session.get('user_id')
        if not user_id:
            return jsonify({'success': False, 'error': '로그인이 필요합니다.'}), 401

        data = request.get_json(force=True, silent=True) or {}
        settlement_order_id = data.get('orderId')  # 정산 결제의 orderNo (0원일 경우 None/빈 문자열)

        # ============ 0) 활성 구독 조회 ============
        subscription = Subscription.query.filter_by(
            user_id=user_id, status=SubscriptionStatus.ACTIVE
        ).first()
        if not subscription:
            return jsonify({'success': False, 'error': '활성 구독이 없습니다.'}), 404

        # 보안: 사용자 소유권 재확인
        if subscription.user_id != user_id:
            logger.warning(f"보안 경고: 사용자 {user_id}가 다른 사용자의 구독 {subscription.id} 처리 시도")
            return jsonify({'success': False, 'error': '권한이 없습니다.'}), 403

        # ============ 1) (있다면) 정산 결제 완료 확정 ============
        if settlement_order_id:
            # PENDING 정산건 PaymentHistory 조회
            settle_ph = PaymentHistory.query.filter_by(
                order_id=settlement_order_id,
                status=PHStatus.PENDING,
                user_id=user_id
            ).first()

            if not settle_ph:
                return jsonify({
                    'success': False,
                    'error': '정산 결제 내역(PENDING)을 찾을 수 없습니다. 이미 처리되었거나 잘못된 요청입니다.'
                }), 400

            pay_token = settle_ph.payment_key

            # Toss 상태 조회 유틸 (내부)
            def _fetch_toss_status(pay_token=None, order_no=None):
                try:
                    if not pay_token and not order_no:
                        return None, "payToken 또는 orderNo 중 하나는 필요합니다."
                    resp = requests.post(
                        TOSS_CONFIG['status_url'],
                        headers={'Content-Type': 'application/json'},
                        json={'apiKey': TOSS_CONFIG['api_key'],
                              'payToken': pay_token,
                              'orderNo': order_no},
                        timeout=10
                    )
                    if resp.status_code == 200:
                        return resp.json(), None
                    return None, f'status 조회 실패: {resp.status_code} {resp.text}'
                except Exception as e:
                    return None, str(e)

            status_data, err = _fetch_toss_status(pay_token=pay_token, order_no=settlement_order_id)
            if err:
                current_app.logger.error(f'[FinishSettle] toss status error: {err}')
                return jsonify({'success': False, 'error': f'정산 결제 상태 확인 실패: {err}'}), 502

            pay_status = status_data.get('payStatus') or status_data.get('status')
            if pay_status != 'PAY_COMPLETE':
                return jsonify({
                    'success': False,
                    'error': f'정산 결제가 완료되지 않았습니다. (상태: {pay_status})'
                }), 400

            # 정산건 확정
            settle_ph.status = PHStatus.COMPLETED
            settle_ph.updated_at = datetime.now()
            db.session.commit()
            current_app.logger.info(f'[FinishSettle] settlement COMPLETED: orderId={settlement_order_id}')

        else:
            # 0원 정산: 별도 검증 없이 통과
            current_app.logger.info('[FinishSettle] settlement is zero-amount; skipping payment verification')

        # ============ 2) 체인 전체 환불 수행 ============
        completed_list = (PaymentHistory.query
                          .filter_by(subscription_id=subscription.id, status=PHStatus.COMPLETED)
                          .order_by(PaymentHistory.created_at.desc())
                          .all())

        def _is_usage_settlement(ph: PaymentHistory) -> bool:
            oid = (ph.order_id or "")
            name = (ph.product_name or "")
            # mlink_usage_* 규칙, onetime 주기, 명칭 등으로 폭넓게 방어
            return (
                oid.startswith("mlink_usage_")
                or (ph.billing_cycle or "").lower() == "onetime"
                or "정산" in name  # '반품 정산' / '구독 사용분 정산' 등
            )

        # 정산건(방금 COMPLETED로 바뀐 onetime 결제)은 환불 대상 아님
        completed_list = [
            ph for ph in completed_list
            if not _is_usage_settlement(ph)
            and not (settlement_order_id and ph.order_id == settlement_order_id)
        ]

        if not completed_list:
            current_app.logger.info('[FinishSettle] 환불할 COMPLETED 결제가 없습니다.')
            # 그래도 구독 해지 진행
            # 크레딧 기준 구독자인 경우 남은 크레딧 사용 완료 처리
            try:
                from src.models.user import SubscriptionType
                from src.utils.credit_manager import consume_all_remaining_credits
                
                user = resolve_user_ref(subscription.user_id)
                if user and user.subscription_type == SubscriptionType.CREDIT:
                    credit_result = consume_all_remaining_credits(
                        user_id=subscription.user_id,
                        description='구독 취소로 인한 남은 크레딧 사용 완료',
                        reference_id=str(subscription.id),
                        reference_type='subscription_cancellation'
                    )
                    if credit_result.get('success'):
                        current_app.logger.info(f'[FinishSettle] 남은 크레딧 사용 완료: {credit_result.get("consumed", 0)} 크레딧')
                    else:
                        current_app.logger.warning(f'[FinishSettle] 남은 크레딧 사용 실패: {credit_result.get("error")}')
            except Exception as e:
                current_app.logger.warning(f'[FinishSettle] 크레딧 사용 처리 실패(무시): {str(e)}')
            
            subscription.status = SubscriptionStatus.CANCELLED
            subscription.next_billing_date = None
            subscription.end_date = datetime.now()
            subscription.cancelled_at = datetime.now()
            _reset_subscription_credit_fields(subscription)  # 크레딧 관련 필드 초기화
            subscription.updated_at = datetime.now()
            db.session.commit()
            try:
                u = resolve_user_ref(subscription.user_id)
                if u:
                    u.role = 'role_free'
                    db.session.commit()
            except Exception:
                pass
            return jsonify({'success': True, 'refund': {'total_refund': 0, 'items': []}}), 200

        total_refund = 0
        items = []

        for ph in completed_list:
            if _is_usage_settlement(ph):
                current_app.logger.info(f"[FinishSettle] skip usage settlement PH: id={ph.id}, order={ph.order_id}")
                continue

            amount = int(ph.amount or 0)
            if amount <= 0:
                continue

            refund_no = f'refund_{uuid.uuid4().hex[:16]}'
            taxable = int(amount / 1.1)
            vat = amount - taxable
            refund_payload = {
                'apiKey': TOSS_CONFIG['api_key'],
                'payToken': ph.payment_key,
                'refundNo': refund_no,
                'amount': amount,
                'amountTaxable': taxable,
                'amountTaxFree': 0,
                'amountVat': vat,
                'amountServiceFee': 0,
                'reason': '반품 취소(체인 전체 환불)'
            }

            try:
                r = requests.post(
                    TOSS_CONFIG['refund_url'],
                    headers={'Content-Type': 'application/json'},
                    json=refund_payload,
                    timeout=30
                )
                if r.status_code == 200:
                    ph.status = PHStatus.REFUNDED
                    ph.description = f'환불 완료 - {refund_no}'
                    db.session.commit()

                    # 해당 결제가 부여한 일수만큼 end_date 회수
                    if ph.subscription_days:
                        subscription.end_date = max(
                            subscription.start_date,
                            subscription.end_date - timedelta(days=int(ph.subscription_days))
                        )
                        subscription.updated_at = datetime.now()
                        db.session.commit()

                    total_refund += amount
                    items.append({'payment_history_id': ph.id, 'amount': amount,
                                  'refund_no': refund_no, 'success': True})
                else:
                    current_app.logger.error(f'[FinishSettle] 환불 실패: ph_id={ph.id} {r.status_code} {r.text}')
                    return jsonify({
                        'success': False,
                        'error': '일부 환불 실패로 중단되었습니다.',
                        'failed_payment_id': ph.id,
                        'raw': r.text
                    }), 502

            except Exception as e:
                current_app.logger.exception('[FinishSettle] 환불 예외')
                return jsonify({
                    'success': False,
                    'error': f'환불 처리 중 오류: {str(e)}',
                    'failed_payment_id': ph.id
                }), 500

        # ============ 3) 크레딧 기준 구독자인 경우 남은 크레딧 사용 완료 처리 ============
        try:
            from src.models.user import SubscriptionType
            from src.utils.credit_manager import consume_all_remaining_credits
            
            user = resolve_user_ref(subscription.user_id)
            if user and user.subscription_type == SubscriptionType.CREDIT:
                credit_result = consume_all_remaining_credits(
                    user_id=subscription.user_id,
                    description='구독 취소로 인한 남은 크레딧 사용 완료',
                    reference_id=str(subscription.id),
                    reference_type='subscription_cancellation'
                )
                if credit_result.get('success'):
                    current_app.logger.info(f'[FinishSettle] 남은 크레딧 사용 완료: {credit_result.get("consumed", 0)} 크레딧')
                else:
                    current_app.logger.warning(f'[FinishSettle] 남은 크레딧 사용 실패: {credit_result.get("error")}')
        except Exception as e:
            current_app.logger.warning(f'[FinishSettle] 크레딧 사용 처리 실패(무시): {str(e)}')

        # ============ 4) 구독 해지 & 권한 회수 ============
        subscription.status = SubscriptionStatus.CANCELLED
        subscription.next_billing_date = None
        subscription.end_date = datetime.now()
        subscription.cancelled_at = datetime.now()
        _reset_subscription_credit_fields(subscription)  # 크레딧 관련 필드 초기화
        subscription.updated_at = datetime.now()
        db.session.commit()

        try:
            u = resolve_user_ref(subscription.user_id)
            if u:
                u.role = 'role_free'
                db.session.commit()
        except Exception as e:
            current_app.logger.warning(f'[FinishSettle] 역할 회수 실패(무시): {str(e)}')

        return jsonify({
            'success': True,
            'message': '정산 결제 확인 후 전체 환불 및 구독 해지가 완료되었습니다.',
            'refund': {
                'total_refund': total_refund,
                'items': items
            }
        }), 200

    except Exception as e:
        db.session.rollback()
        current_app.logger.exception('[FinishSettle] unexpected error')
        return jsonify({'success': False, 'error': str(e)}), 500

# 예상 환불/정산 금액
@subscription_bp.route('/api/subscription/cancel-estimate', methods=['GET'])
def estimate_cancel_amounts():
    """
    여러 시나리오의 예상 금액을 한 번에 제공.
    쿼리:
      - new_product_id (옵션): 캐리오버(플랜 변경) 시뮬레이션용
      - detailed=1/true   : 결제건별 브레이크다운 포함
    보안: 사용자 소유권 검증
    """
    try:
        user_id = session.get('user_id')
        if not user_id:
            return jsonify({'success': False, 'error': '로그인이 필요합니다.'}), 401

        new_product_id = request.args.get('new_product_id', type=int)
        detailed = _is_truthy(request.args.get('detailed'))

        sub = Subscription.query.filter_by(
            user_id=user_id, status=SubscriptionStatus.ACTIVE
        ).first()

        # 보안: 구독이 있는 경우 소유권 검증
        if sub and sub.user_id != user_id:
            logger.warning(f"보안 경고: 사용자 {user_id}가 다른 사용자의 구독 {sub.id} 조회 시도")
            return jsonify({'success': False, 'error': '권한이 없습니다.'}), 403

        if not sub:
            return jsonify({
                'success': True,
                'estimate': {
                    'scenarios': {
                        'settle_then_refund': {'refund_total': 0, 'settlement_amount': 0},
                        'refund_future_only': {'refund_total': 0},
                        'carryover': None
                    },
                    'meta': {'chain_start': None},
                    'breakdown': [] if detailed else None
                }
            }), 200

        # 체인 COMPLETED(정산건 제외) 최신순
        chain_ph = _get_chain_completed_ph(sub)

        # A) 정산 먼저 → 전체 환불
        usage_meta = _calc_usage_charge_for_chain(chain_ph)
        # usage_meta = _calc_usage_charge_for_chain_v2(chain_ph)
        refund_total_chain = sum(int(ph.amount or 0) for ph in chain_ph)

        # B) 미래 구간만 환불
        refund_future_only = _estimate_refund_future_only(chain_ph)

        # C) 캐리오버(플랜 변경)
        carryover = None
        if new_product_id:
            new_product = Product.query.get(new_product_id)
            if new_product:
                carryover = _estimate_carryover_for_current_period(sub, new_product)
                carryover.update({
                    'new_product_id': new_product_id,
                    'new_product_price': int(new_product.price or 0),
                    'new_product_cycle': new_product.billing_cycle
                })

        payload = {
            'success': True,
            'estimate': {
                'scenarios': {
                    'settle_then_refund': {
                        'refund_total': int(refund_total_chain),
                        'settlement_amount': int(usage_meta.get('usage_charge', 0))
                    },
                    'refund_future_only': {
                        'refund_total': int(refund_future_only)
                    },
                    'carryover': carryover  # 없으면 null
                },
                'meta': {
                    'total_paid': int(usage_meta.get('total_paid', 0)),
                    'total_days': int(usage_meta.get('total_days', 0)),
                    'used_days': int(usage_meta.get('used_days', 0)),
                    'chain_start': (usage_meta.get('chain_start').isoformat()
                                    if usage_meta.get('chain_start') else None)
                }
            }
        }

        if detailed:
            payload['estimate']['breakdown'] = _build_chain_breakdown(chain_ph)

        return jsonify(payload), 200

    except Exception as e:
        logger.exception("cancel-estimate failed")
        return jsonify({'success': False, 'error': str(e)}), 500


def _is_settlement_ph(ph) -> bool:
    """정산/일회성 결제 식별(체인 환불 대상에서 제외)."""
    oid = (ph.order_id or '')
    cyc = (ph.billing_cycle or '').lower()
    name = (ph.product_name or '')
    return oid.startswith('mlink_usage_') or cyc == 'onetime' or ('정산' in name)


def _get_chain_completed_ph(subscription):
    """체인 COMPLETED 결제(정산건 제외) 최신순 목록."""
    ph_list = (PaymentHistory.query
               .filter_by(subscription_id=subscription.id, status=PHStatus.COMPLETED)
               .order_by(PaymentHistory.created_at.desc())
               .all())
    return [ph for ph in ph_list if not _is_settlement_ph(ph)]


def _days_for_cycle(bc: str) -> int:
    return 365 if (bc or '').lower() == 'yearly' else 30


def _calc_usage_charge_for_chain_v2(ph_list) -> dict:
    """
    '구간별 일일단가'로 사용가치 합산.
    - 각 결제별 interval = [subscription_start_date, subscription_end_date]
      (없으면 created_at 기준 + 30/365 fallback)
    - interval∩[chain_start..today]의 일수만 사용
    - usage_value += (amount / interval_days) * used_days_in_interval
    """
    if not ph_list:
        return {'total_paid': 0, 'usage_charge': 0, 'chain_start': None,
                'total_days': 0, 'used_days': 0}

    # 체인 시작일 = 모든 구간 start 중 min
    chain_start = None
    today = datetime.now()
    total_paid = 0
    total_days = 0
    used_days_accum = 0
    usage_value = 0.0

    for ph in ph_list:
        amt = float(ph.amount or 0)
        total_paid += amt

        start_dt = ph.subscription_start_date or ph.created_at
        end_dt = ph.subscription_end_date
        if not start_dt:
            # 비상 케이스: created_at도 없으면 오늘 1일 구간
            start_dt = today
        if not end_dt:
            # fallback: billing_cycle 기반 길이
            bc_days = _days_for_cycle(getattr(ph, 'billing_cycle', 'monthly'))
            end_dt = start_dt + timedelta(days=bc_days)

        # 체인 시작
        chain_start = min(chain_start, start_dt) if chain_start else start_dt

        # 이 구간 총일수
        interval_days = max(1, (end_dt.date() - start_dt.date()).days)  # 0 방지
        total_days += interval_days

        # today 기준 사용일수(해당 구간에서 오늘까지)
        # 구간이 미래만 있으면 0, 과거만 있으면 full, 현재 포함이면 부분
        used_in_interval = 0
        if today.date() >= start_dt.date():
            used_in_interval = min(interval_days,
                                   max(0, (min(today.date(), end_dt.date()) - start_dt.date()).days + 1))

        used_days_accum += used_in_interval

        per_day = amt / interval_days
        usage_value += per_day * used_in_interval

    usage_charge = int(round(usage_value))

    return {
        'total_paid': int(total_paid),
        'total_days': int(total_days),
        'used_days': int(used_days_accum),
        'usage_charge': int(usage_charge),
        'chain_start': chain_start
    }


def _estimate_refund_future_only(ph_list) -> int:
    """
    완전한 미래 구간(시작일 > now)만 환불했을 때의 총 환불액.
    """
    if not ph_list:
        return 0
    now = datetime.now()
    total = 0
    for ph in ph_list:
        start_dt = ph.subscription_start_date or ph.created_at
        if start_dt and start_dt > now:
            total += int(ph.amount or 0)
    return int(total)


def _estimate_carryover_for_current_period(subscription, new_product) -> dict:
    """
    현재 진행중인 구간 남은 가치를 새 상품 기준으로 '일수'로 환산(추정치).
    (실환불 없이 계산만)
    """
    today = datetime.now().date()
    cur = (PaymentHistory.query
           .filter(
               PaymentHistory.subscription_id == subscription.id,
               PaymentHistory.status == PHStatus.COMPLETED,
               PaymentHistory.subscription_start_date <= datetime.combine(today, datetime.min.time()),
               PaymentHistory.subscription_end_date >= datetime.combine(today, datetime.min.time()),
           )
           .order_by(PaymentHistory.created_at.desc())
           .first())

    if not cur or not cur.subscription_days or cur.subscription_days <= 0:
        return {"credit_amount": 0, "credit_days": 0}

    total_days = int(cur.subscription_days)
    remaining_days = max(0, (cur.subscription_end_date.date() - today).days)
    if remaining_days <= 0:
        return {"credit_amount": 0, "credit_days": 0}

    credit_amount = int(round(float(cur.amount) * (remaining_days / total_days)))

    new_cycle_days = _days_for_cycle(getattr(new_product, 'billing_cycle', 'monthly'))
    price_per_day_new = (new_product.price or 0) / max(1, new_cycle_days)
    credit_days = int(credit_amount / price_per_day_new) if price_per_day_new > 0 else 0

    return {"credit_amount": int(credit_amount), "credit_days": int(credit_days)}


def _is_truthy(s: str | None) -> bool:
    return str(s).lower() in ('1', 'true', 'yes', 'y')


def _interval_bounds(ph):
    """결제 1건의 [start, end], 총일수 계산 (fallback 포함)."""
    start_dt = ph.subscription_start_date or ph.created_at
    if not start_dt:
        start_dt = datetime.now()
    end_dt = ph.subscription_end_date
    if not end_dt:
        bc = (getattr(ph, 'billing_cycle', '') or '').lower()
        end_dt = start_dt + timedelta(days=365 if bc == 'yearly' else 30)
    interval_days = max(1, (end_dt.date() - start_dt.date()).days)  # 최소 1일
    return start_dt, end_dt, interval_days


def _build_chain_breakdown(ph_list):
    """
    결제건별 상세 브레이크다운 생성:
    - 결제 금액, 주기, interval, 일일단가, interval 내 사용일수, 사용가치
    - 완전미래구간 여부 및 환불가능액(미래구간 환불 시나리오 참고용)
    """
    items = []
    today = datetime.now().date()

    for ph in ph_list:
        start_dt, end_dt, interval_days = _interval_bounds(ph)
        amount = float(ph.amount or 0)
        per_day = amount / interval_days

        # 사용일수(해당 구간에서 오늘까지)
        used_days_in_interval = 0
        if today >= start_dt.date():
            used_days_in_interval = min(
                interval_days,
                max(0, (min(today, end_dt.date()) - start_dt.date()).days + 1),
            )

        usage_value = per_day * used_days_in_interval

        fully_future = start_dt.date() > today
        refundable_future_amount = int(amount) if fully_future else 0

        items.append({
            'payment_history_id': ph.id,
            'order_id': ph.order_id,
            'product_name': ph.product_name,
            'billing_cycle': (ph.billing_cycle or '').lower(),
            'amount': int(amount),
            'start_date': start_dt.isoformat(),
            'end_date': end_dt.isoformat(),
            'interval_days': int(interval_days),
            'per_day': float(round(per_day, 6)),          # 소수점 6자리까지
            'used_days_in_interval': int(used_days_in_interval),
            'usage_value': int(round(usage_value)),
            'fully_future': bool(fully_future),
            'refundable_future_amount': int(refundable_future_amount),
        })

    return items

@subscription_bp.route('/api/payments/status')
def payment_status():
    order_id = request.args.get('orderId')
    if not order_id:
        return jsonify(success=False, error='orderId required'), 400
    ph = PaymentHistory.query.filter_by(order_id=order_id).order_by(PaymentHistory.created_at.desc()).first()
    if not ph:
        return jsonify(success=False, status='NOT_FOUND'), 404
    return jsonify(success=True, status=ph.status.value.lower())


@subscription_bp.post("/api/subscription/early-renew")
def early_renew():
    """조기 갱신 - 보안: 사용자 소유권 검증"""
    user_id = session.get('user_id')
    if not user_id:
        return jsonify({'success': False, 'error': '로그인이 필요합니다.'}), 401

    sub = Subscription.query.filter_by(user_id=user_id, status='ACTIVE').first()
    if not sub:
        return jsonify(success=False, error="활성 구독이 없습니다."), 400

    # 보안: 사용자 소유권 재확인
    if sub.user_id != user_id:
        logger.warning(f"보안 경고: 사용자 {user_id}가 다른 사용자의 구독 {sub.id} 갱신 시도")
        return jsonify({'success': False, 'error': '권한이 없습니다.'}), 403

    # 마지막 COMPLETED 결제
    last_ph = (PaymentHistory.query
               .filter_by(subscription_id=sub.id, status='COMPLETED')
               .order_by(PaymentHistory.subscription_end_date.desc())
               .first())
    if not last_ph or not last_ph.subscription_end_date:
        return jsonify(success=False, error="마지막 결제 내역이 올바르지 않습니다."), 400

    # 이미 미래 구간이 존재하는지(중복 결제 방지)
    future_exists = (PaymentHistory.query
                     .filter(PaymentHistory.subscription_id==sub.id,
                             PaymentHistory.subscription_start_date > last_ph.subscription_end_date,
                             PaymentHistory.status.in_(('PENDING','COMPLETED')))
                     .first())
    if future_exists:
        return jsonify(success=False, error="이미 다음 구간 결제가 존재합니다."), 409

    cycle = sub.billing_cycle or sub.product.billing_cycle or 'monthly'
    cycle_days = 30 if cycle=='monthly' else 365

    next_start = last_ph.subscription_end_date + timedelta(days=1)   # 다음날부터
    next_end   = next_start + timedelta(days=cycle_days-1)

    amount = sub.product.price_year if cycle=='yearly' else sub.product.price

    order_id = f"mlink_renew_{sub.id}_{int(time.time())}"

    # 부가세 계산
    amount_taxable = int(round(amount / 1.1))
    amount_vat = amount - amount_taxable

    payment_data = {
        'orderNo': order_id,
        'amount': amount,
        'amountTaxFree': 0,
        'amountTaxable': amount_taxable,
        'amountVat': amount_vat,
        'productDesc': sub.product.name,
        'apiKey': TOSS_CONFIG['api_key'],
        'autoExecute': True,
        'resultCallback': 'https://pay.toss.im/payfront/demo/callback',
        'callbackVersion': 'V2',
        'retUrl': f'{BACK_URL}/api/v2/payments/toss/success?orderno={order_id}&status=PAY_COMPLETE',
        'retCancelUrl': f'{FRONT_URL}/payment/fail?code=PAY_PROCESS_CANCELED&message=결제가 취소되었습니다&orderId={order_id}',
    }
    r = requests.post(
        TOSS_CONFIG['base_url'],
        headers={'Content-Type': 'application/json'},
        json=payment_data,
        timeout=10
    )

    if r.status_code != 200:
        try:
            data = r.json()
        except Exception:
            data = {"message": r.text}
        return jsonify(success=False, error=f"Toss 준비 실패: {data}"), 502

    result = r.json()
    pay_token = result.get('payToken')
    checkout_url = result.get('checkoutPage', '')

    # 임시 PaymentHistory 생성 (PENDING)
    temp_ph = PaymentHistory(
        user_id=user_id,
        subscription_id=sub.id,
        payment_key=pay_token,
        order_id=order_id,
        amount=amount,
        status=PaymentStatus.PENDING,
        payment_method=PaymentMethod.TOSS_PAYMENTS,
        product_id=sub.product_id,
        product_name=sub.product.name,
        product_price=amount,
        billing_cycle=cycle, 
        subscription_days=cycle_days,
        subscription_start_date=next_start,
        subscription_end_date=next_end,
        created_at=datetime.now()
    )
    db.session.add(temp_ph)
    db.session.commit()

    return jsonify(
        success=True,
        payment={
            "paymentKey": pay_token,
            "orderId": order_id,
            "amount": amount,
            "orderName": sub.product.name,
            "payUrl": checkout_url
        },
        period={
            "start": next_start.isoformat(),
            "end": next_end.isoformat(),
            "cycle_days": cycle_days
        }
    ), 200
     