
    iNZ                         d Z ddlZddlmZ ddlmZ ddlZddlmZ ddl	m
Z
 ddlmZ ddlmZ dd	lmZ dd
lmZ ddlmZ ddlmZ ddlmZ ddlmZ  ee      ZdZdZg dZ G d d      Z y)u   
Meta Marketing API 클라이언트 모듈

Usage:
    from utils.meta_ads_client import MetaAdsClient
    client = MetaAdsClient()
    campaigns = client.list_campaigns()
    N)Path)Any)Ad)	AdAccount)
AdCreative)AdImage)AdSet)Campaign)FacebookAdsApi)FacebookRequestError)load_env_keys)
get_loggerz https://graph.facebook.com/v25.0z/home/jay/workspace/.env.keys)impressionsclicksspendcpccpmctractionsc                   B   e Zd ZdZefdeddfdZdedededdfd	Zed
e	de
fd       ZdefdZdeddfdZde
fdZde
fdZ	 	 d3dee   dz  dedee
   fdZ	 d4dedee   dz  de
fdZ	 	 	 d5dededededz  dee   dz  de
fdZdede	de
fdZdedefdZ	 	 	 d6dedz  dee   dz  dedee
   fdZ	 d7dededed e
d!ed"edede
fd#Zd$ede	de
fd%Zd$edefd&Zd'edefd(Z	 d4ded)ed*ed+ed,edz  de
fd-Z	 	 	 	 d8d.ed/edee   dz  d0edz  d1e
dz  dee
   fd2Zy)9MetaAdsClientu"   Meta Marketing API 클라이언트env_keys_pathreturnNc                 N   || _         t        |       g d}|D cg c]$  }t        j                  j	                  |      r#|& }}|rt        d|       t        j                  d   | _        t        j                  d   | _        t        j                  d   | _        t        j                  d   | _	        | j                  | j                  | j                  | j                         t        | j                        | _        t        j                  d| j                         yc c}w )	u   
        MetaAdsClient 초기화.

        Args:
            env_keys_path: .env.keys 파일 경로 (기본값: /home/jay/workspace/.env.keys)

        Raises:
            ValueError: 필수 환경변수가 누락된 경우
        )META_APP_IDMETA_APP_SECRETMETA_ACCESS_TOKENMETA_AD_ACCOUNT_IDu   필수 환경변수 누락: r   r   r   r   u.   MetaAdsClient 초기화 완료 (account_id=%s)N)_env_keys_pathr   osenvironget
ValueError_app_id_app_secret_access_token_ad_account_id	_init_apir   _accountloggerinfo)selfr   required_keyskmissings        F/home/jay/workspace/.worktrees/task-2116-dev1/utils/meta_ads_client.py__init__zMetaAdsClient.__init__/   s     ,m$
 ,E2::>>!3D1EE;G9EFFzz-0::&78ZZ(;< jj)=>t||T%5%5t7I7IJ!$"5"56DdFYFYZ Fs   $D" D"app_id
app_secretaccess_tokenc                 \    t        j                  |||       t        j                  d       y)u   FacebookAdsApi 초기화.u   FacebookAdsApi 초기화 완료N)r   initr*   debug)r,   r2   r3   r4   s       r0   r(   zMetaAdsClient._init_apiT   s!    FJ=67    objc                 P    t        | d      r| j                         S t        |       S )u+   SDK 반환 객체를 순수 dict로 변환.export_all_data)hasattrr;   dict)r9   s    r0   _to_dictzMetaAdsClient._to_dictY   s(     3)*&&((Cyr8   c                    t          d}d| j                  | j                  | j                  d}t        j                  d       t        j                  ||d      }|j                          |j                         }d|vrt        d|j                  d	             |d   }|| _        |t        j                  d
<   | j                  | j                  | j                  |       t        j                  d|j                  d             |S )un  
        단기 토큰을 장기 토큰(60일)으로 교환한다.

        GET https://graph.facebook.com/v25.0/oauth/access_token 직접 호출.

        Returns:
            str: 새로 발급된 장기 액세스 토큰

        Raises:
            requests.HTTPError: API 호출 실패 시
            ValueError: 응답에 access_token이 없을 경우
        z/oauth/access_tokenfb_exchange_token)
grant_type	client_idclient_secretr@   u   장기 토큰 교환 요청 중   paramstimeoutr4   u-   토큰 교환 응답에 access_token 없음: errorr   u+   장기 토큰 교환 성공 (token_type=%s)
token_type)_GRAPH_API_BASEr$   r%   r&   r*   r7   requestsr"   raise_for_statusjsonr#   r    r!   r(   r+   )r,   urlrF   respdata	new_tokens         r0   exchange_tokenzMetaAdsClient.exchange_tokend   s     !!!45-!--!%!3!3	
 	67||C;yy{%LTXXV]M^L_`aa(	&*3

&'t||T%5%5yAA488LCYZr8   rQ   c                    t        | j                        }|j                         st        j	                  d|       y|j                  d      j                  d      }d}g }|D ]  }|j                         }d}d	}	|j                  |      r|j                  d| d
       d}@|j                  |	      r)|j                  d      s|j                  d	| d
       d}z|j                  |        |s*|j                  d| d
       t        j	                  d       |j                  dj                  |      d       |t        j                  d<   || _        t        j                  d       y)u   
        .env.keys 파일의 META_ACCESS_TOKEN 값을 업데이트한다.

        파일 전체를 읽고 해당 라인만 교체 후 덮어쓴다.

        Args:
            new_token: 교체할 새 액세스 토큰 문자열
        u$   .env.keys 파일이 없습니다: %sNzutf-8)encodingT)keependsFzexport META_ACCESS_TOKEN=zMETA_ACCESS_TOKEN=
exportz
export META_ACCESS_TOKEN=uC   META_ACCESS_TOKEN 라인이 없어 파일 끝에 추가했습니다 r   u9   .env.keys 파일의 META_ACCESS_TOKEN 업데이트 완료)r   r   existsr*   warning	read_text
splitlinesstrip
startswithappend
write_textjoinr    r!   r&   r+   )
r,   rQ   pathlinesupdated	new_lineslinestrippedprefix_exportprefix_plains
             r0   update_env_tokenzMetaAdsClient.update_env_token   sS    D''({{}NNA4H0;;T;J	 	'Dzz|H7M/L""=1  #<YKr!JK$$\28;N;Nx;X  #5i[!CD  &	' :9+RHINN`a	*W=*3

&'&OPr8   c                 p   t          d}| j                  | j                   d| j                   d}t        j                  d       t        j                  ||d      }|j                          |j                         }|j                  d|      }t        j                  d|j                  d	             |S )
u   
        현재 액세스 토큰의 유효성을 확인한다.

        debug_token 엔드포인트를 사용하여 is_valid, expires_at, scopes 등을 반환한다.

        Returns:
            dict: Graph API debug_token 응답의 data 필드
        z/debug_token|)input_tokenr4   u   토큰 유효성 확인 중rD   rE   rP   u   토큰 유효성: is_valid=%sis_valid)
rJ   r&   r$   r%   r*   r7   rK   r"   rL   rM   )r,   rN   rF   rO   resultrP   s         r0   check_tokenzMetaAdsClient.check_token   s     !!.--#||nAd.>.>-?@
 	23||C;zz&&)4dhhz6JKr8   c                 :   t         j                  j                  t         j                  j                  t         j                  j                  t         j                  j
                  t         j                  j                  t         j                  j                  t         j                  j                  g}	 | j                  j                  |      }t        j                  d       | j                  |      S # t        $ r}t        j                  d|        d}~ww xY w)u   
        광고 계정 상태, 잔액, 스펜딩 한도 정보를 반환한다.

        Returns:
            dict: account_status, balance, spend_cap, amount_spent,
                  currency, timezone_name, name 포함 dict
        fieldsu   계정 정보 조회 완료u   계정 정보 조회 실패: %sN)r   Fieldnameaccount_statusamount_spentbalance	spend_capcurrencytimezone_namer)   api_getr*   r7   r>   r   rH   )r,   rs   rP   es       r0   get_account_infozMetaAdsClient.get_account_info   s     OO  OO**OO((OO##OO%%OO$$OO))
	==(((7DLL67==&&# 	LL:A>	s   3AC5 5	D>DDrs   limitc                    |t         j                  j                  t         j                  j                  t         j                  j                  t         j                  j
                  t         j                  j                  t         j                  j                  t         j                  j                  g}	 | j                  j                  |d|i      }t        |      D cg c]  }| j                  |       }}t        j                  dt        |             |S c c}w # t         $ r}t        j#                  d|        d}~ww xY w)u  
        광고 계정의 캠페인 목록을 반환한다.

        Args:
            fields: 반환할 필드 목록. None이면 기본 필드 사용.
            limit: 최대 반환 개수 (기본값 25)

        Returns:
            list[dict]: 캠페인 정보 목록
        Nr   rs   rF   u%   캠페인 목록 조회 완료: %d건u"   캠페인 목록 조회 실패: %s)r
   rt   ru   status	objectivedaily_budgetlifetime_budgetcreated_timeupdated_timer)   get_campaignslistr>   r*   r7   lenr   rH   )r,   rs   r   	campaignscro   r}   s          r0   list_campaignszMetaAdsClient.list_campaigns   s     >##%%((++..++++F
	33' 4 I 15Y@1dmmA&@F@LL@#f+NM A $ 	LL=qA	s*   5,D! !D9"D! D! !	E*EEcampaign_idc                 D   |t         j                  j                  t         j                  j                  t         j                  j                  t         j                  j
                  t         j                  j                  t         j                  j                  t         j                  j                  g}	 t        |      j                  |      }| j                  |      }t        j                  d|       |S # t        $ r}t        j                  d||        d}~ww xY w)u   
        단일 캠페인 정보를 반환한다.

        Args:
            campaign_id: 캠페인 ID
            fields: 반환할 필드 목록. None이면 기본 필드 사용.

        Returns:
            dict: 캠페인 정보
        Nrr   u   캠페인 조회 완료: id=%su#   캠페인 조회 실패 (id=%s): %s)r
   rt   ru   r   r   r   r   r   r   r|   r>   r*   r7   r   rH   )r,   r   rs   campaignro   r}   s         r0   get_campaignzMetaAdsClient.get_campaign  s     >##%%((++..++++F	,44F4CH]]8,FLL9;GM# 	LL>QO	s   5AC9 9	DDDru   r   r   r   special_ad_categoriesc                    t         j                  j                  |t         j                  j                  |t         j                  j                  |d||ng i}|||t         j                  j
                  <   	 | j                  j                  g |      }| j                  |      }t        j                  d|j                  d      |       |S # t        $ r}	t        j                  d|	        d}	~	ww xY w)u  
        새 캠페인을 생성한다.

        Args:
            name: 캠페인 이름
            objective: 캠페인 목표 (예: OUTCOME_AWARENESS, OUTCOME_TRAFFIC)
            status: 초기 상태 (기본값: PAUSED)
            daily_budget: 일일 예산 (센트/원 단위). None이면 설정하지 않음.
            special_ad_categories: 특수 광고 카테고리 목록. None이면 빈 리스트.

        Returns:
            dict: 생성된 캠페인 정보 (id, name 등)
        r   Nr   u&   캠페인 생성 완료: id=%s name=%sidu   캠페인 생성 실패: %s)r
   rt   ru   r   r   r   r)   create_campaignr>   r*   r+   r"   r   rH   )
r,   ru   r   r   r   r   rF   r   ro   r}   s
             r0   r   zMetaAdsClient.create_campaign-  s    , NNNN$$iNN!!6#>S>_%:eg	"
 #2>F8>>../	}}44Bv4NH]]8,FKK@&**TBRTXYM# 	LL6:	s   7AC 	C2C--C2rF   c                    	 t        |      }|j                  |      }t        j                  d|t	        |j                                      | j                  |      S # t        $ r}t        j                  d||        d}~ww xY w)u   
        기존 캠페인을 업데이트한다.

        Args:
            campaign_id: 캠페인 ID
            **params: 업데이트할 필드 (name, status, daily_budget 등)

        Returns:
            dict: API 응답
        rF   u.   캠페인 업데이트 완료: id=%s fields=%su)   캠페인 업데이트 실패 (id=%s): %sN)	r
   
api_updater*   r+   r   keysr>   r   rH   )r,   r   rF   r   ro   r}   s         r0   update_campaignzMetaAdsClient.update_campaignT  sy    	,H(((7FKKH+W[\b\g\g\iWjk==((# 	LLDkSTU	   AA 	B'A??Bc                     	 t        |      j                          t        j                  d|       y# t        $ r}t        j                  d||        d}~ww xY w)u   
        캠페인을 삭제한다 (상태를 DELETED로 설정).

        Args:
            campaign_id: 삭제할 캠페인 ID

        Returns:
            bool: 삭제 성공 여부
        u   캠페인 삭제 완료: id=%sTu#   캠페인 삭제 실패 (id=%s): %sN)r
   
api_deleter*   r+   r   rH   )r,   r   r}   s      r0   delete_campaignzMetaAdsClient.delete_campaignh  sN    	[!,,.KK8+F# 	LL>QO	   /2 	AAAc                    |t         j                  j                  t         j                  j                  t         j                  j                  t         j                  j
                  t         j                  j                  t         j                  j                  t         j                  j                  t         j                  j                  g}d|i}	 |rt        |      j                  ||      }n| j                  j                  ||      }t        |      D cg c]  }| j                  |       }}t        j!                  dt#        |             |S c c}w # t$        $ r}t        j'                  d|        d}~ww xY w)u  
        광고세트 목록을 반환한다.

        Args:
            campaign_id: 지정 시 해당 캠페인의 광고세트만 반환.
                         None이면 계정 전체 광고세트 반환.
            fields: 반환할 필드 목록. None이면 기본 필드 사용.
            limit: 최대 반환 개수 (기본값 25)

        Returns:
            list[dict]: 광고세트 정보 목록
        Nr   r   u(   광고세트 목록 조회 완료: %d건u%   광고세트 목록 조회 실패: %s)r	   rt   ru   r   r   	targetingoptimization_goalbilling_event
start_timeend_timer
   get_ad_setsr)   r   r>   r*   r7   r   r   rH   )	r,   r   rs   r   rF   adsetsaro   r}   s	            r0   list_adsetszMetaAdsClient.list_adsets~  s!   $ >  ""((%%--))&&$$	F 5!
	!+.::&QW:X22&2P04V=1dmmA&=F=LLCS[QM > $ 	LL@!D	s+   A	E E3"E E 	F $E;;F r   r   r   c                 p   t         j                  j                  |t         j                  j                  |t         j                  j                  |t         j                  j
                  |t         j                  j                  |t         j                  j                  |t         j                  j                  |i}	 | j                  j                  g |      }	| j                  |	      }
t        j                  d|
j                  d      |       |
S # t        $ r}t        j!                  d|        d}~ww xY w)u  
        광고세트를 생성한다.

        Args:
            campaign_id: 상위 캠페인 ID
            name: 광고세트 이름
            daily_budget: 일일 예산 (센트/원 단위)
            targeting: 타겟팅 설정 dict
            optimization_goal: 최적화 목표 (예: REACH, LINK_CLICKS)
            billing_event: 과금 이벤트 (예: IMPRESSIONS, LINK_CLICKS)
            status: 초기 상태 (기본값: PAUSED)

        Returns:
            dict: 생성된 광고세트 정보
        r   u)   광고세트 생성 완료: id=%s name=%sr   u   광고세트 생성 실패: %sN)r	   rt   ru   r   r   r   r   r   r   r)   create_ad_setr>   r*   r+   r"   r   rH   )r,   r   ru   r   r   r   r   r   rF   adsetro   r}   s               r0   create_adsetzMetaAdsClient.create_adset  s    4 KKdKK##[KK$$lKK!!9KK))+<KK%%}KK"
	MM//r&/IE]]5)FKKCVZZPTEUW[\M# 	LL91=	s   :AD 	D5D00D5adset_idc                    	 t        |      }|j                  |      }t        j                  d|t	        |j                                      | j                  |      S # t        $ r}t        j                  d||        d}~ww xY w)u   
        기존 광고세트를 업데이트한다.

        Args:
            adset_id: 광고세트 ID
            **params: 업데이트할 필드 (name, status, daily_budget, targeting 등)

        Returns:
            dict: API 응답
        r   u1   광고세트 업데이트 완료: id=%s fields=%su,   광고세트 업데이트 실패 (id=%s): %sN)	r	   r   r*   r+   r   r   r>   r   rH   )r,   r   rF   r   ro   r}   s         r0   update_adsetzMetaAdsClient.update_adset  sx    	(OE%%V%4FKKKXW[\b\g\g\iWjk==((# 	LLGSTU	r   c                     	 t        |      j                          t        j                  d|       y# t        $ r}t        j                  d||        d}~ww xY w)u   
        광고세트를 삭제한다.

        Args:
            adset_id: 삭제할 광고세트 ID

        Returns:
            bool: 삭제 성공 여부
        u!   광고세트 삭제 완료: id=%sTu&   광고세트 삭제 실패 (id=%s): %sN)r	   r   r*   r+   r   rH   )r,   r   r}   s      r0   delete_adsetzMetaAdsClient.delete_adset  sM    	(O&&(KK;XF# 	LLA8QO	r   
image_pathc                    t        |      }|j                         st        d|       t        j                  j
                  t        |      i}	 | j                  j                  g |      }| j                  |      }d|v r|d   }t        t        |            }||   }|j                  dd      }t        j                  d|j                  |       |S # t         $ r'}	t        j#                  d|j                  |	        d}	~	ww xY w)	u  
        이미지 파일을 Meta 광고 이미지로 업로드하고 hash를 반환한다.

        Args:
            image_path: 업로드할 이미지 파일의 절대/상대 경로

        Returns:
            str: 업로드된 이미지의 hash 값
        u-   이미지 파일을 찾을 수 없습니다: r   imageshashrX   u(   이미지 업로드 완료: %s (hash=%s)u#   이미지 업로드 실패 (%s): %sN)r   rY   FileNotFoundErrorr   rt   filenamestrr)   create_ad_imager>   nextiterr"   r*   r+   ru   r   rH   )
r,   r   rb   rF   imagerP   inner	first_key
image_hashr}   s
             r0   upload_imagezMetaAdsClient.upload_image  s     J{{}#&ST^S_$`aa--((#d)4	MM11F1KE=='D4X e-	Y'&"-JKKBDIIzZ# 	LL>		1M	s   BC 	D"D  Dr   page_idmessagelinkc                    ||d}|r||d<   ||d}t         j                  j                  |t         j                  j                  |i}	 | j                  j                  g |      }	| j                  |	      }
t        j                  d|
j                  d      |       |
S # t        $ r}t        j                  d|        d}~ww xY w)	uw  
        광고 크리에이티브를 생성한다.

        Args:
            name: 크리에이티브 이름
            image_hash: 업로드된 이미지의 hash 값
            page_id: 페이스북 페이지 ID
            message: 광고 본문 텍스트
            link: 연결할 URL (선택)

        Returns:
            dict: 생성된 AdCreative 정보
        )r   r   r   )r   	link_datar   u6   광고 크리에이티브 생성 완료: id=%s name=%sr   u+   광고 크리에이티브 생성 실패: %sN)r   rt   ru   object_story_specr)   create_ad_creativer>   r*   r+   r"   r   rH   )r,   ru   r   r   r   r   r   r   rF   creativero   r}   s               r0   create_creativezMetaAdsClient.create_creative  s    , $%
	  $If "
 !!4..0A"
	}}77r&7QH]]8,FKKPRXR\R\]aRbdhiM# 	LLFJ	s   	AB 	C(B??C	object_idobject_typedate_preset
time_rangec                    |t         }i }|r||d<   n|r||d<   nd|d<   	 |j                         }|dk(  rt        |      }n1|dk(  rt        |      }n |dk(  rt	        |      }nt        d| d	      |j                  ||
      }	t        |	      D 
cg c]  }
| j                  |
       }}
t        j                  dt        |      ||       |S c c}
w # t        $ r}t        j                  d|||        d}~ww xY w)u  
        광고 성과 인사이트를 반환한다.

        Args:
            object_id: 조회할 캠페인/광고세트/광고 ID
            object_type: 'campaign', 'adset', 'ad' 중 하나 (기본값: 'campaign')
            fields: 반환할 필드 목록. None이면 기본 필드 사용.
            date_preset: 기간 프리셋 (예: 'last_7d', 'last_30d', 'today').
                         time_range와 동시 사용 불가.
            time_range: 기간 dict (예: {"since": "2024-01-01", "until": "2024-01-31"}).
                        date_preset과 동시 사용 불가.

        Returns:
            list[dict]: 인사이트 레코드 목록
        Nr   r   last_7dr   r   adu!   지원하지 않는 object_type: u7   . 'campaign', 'adset', 'ad' 중 하나여야 합니다.r   u9   인사이트 조회 완료: %d건 (object_type=%s, id=%s)u6   인사이트 조회 실패 (object_type=%s, id=%s): %s)_DEFAULT_INSIGHT_FIELDSlowerr
   r	   r   r#   get_insightsr   r>   r*   r7   r   r   rH   )r,   r   r   rs   r   r   rF   obj_type_lowertargetinsightsiro   r}   s                r0   r   zMetaAdsClient.get_insightsM  s$   . >,F!#$/F=!#-F< $-F=!	(..0N+!),7*y)4'I 7}D{|  **&*HH04X?1dmmA&?F?LLKF	 M @ $ 	LLQS^`iklm	s*   A2C C+$C C 	C<C77C<)N   )N)PAUSEDNN)NNr   )r   )r   NNN)__name__
__module____qualname____doc___ENV_KEYS_PATHr   r1   r(   staticmethodr   r=   r>   rR   rj   rp   r~   r   intr   r   r   r   boolr   r   r   r   r   r   r   r    r8   r0   r   r   ,   s   ,,: [c [t [J8 8 8C 8D 8
 c d  " "H%Q# %Q$ %QNT 4$ > $(#S	D # # 
d	#P $(   S	D   
	 L #'26%% % 	%
 Dj%  $Cy4/% 
%N3 # $ (3 4 0 #'#'	(4Z( S	D ( 	(
 
d(d )) ) 	)
 ) ) ) ) 
)VS C D (S T ,s s H  ,, , 	,
 , Dj, 
,j &#'"&"&:: : S	D 	:
 4Z: 4K: 
d:r8   r   )!r   r    pathlibr   typingr   rK   facebook_business.adobjects.adr   %facebook_business.adobjects.adaccountr   &facebook_business.adobjects.adcreativer   #facebook_business.adobjects.adimager   !facebook_business.adobjects.adsetr	   $facebook_business.adobjects.campaignr
   facebook_business.apir   facebook_business.exceptionsr   utils.env_loaderr   utils.loggerr   r   r*   rJ   r   r   r   r   r8   r0   <module>r      s^    
    - ; = 7 3 9 0 = * #	H	40 [	 [	r8   