#!/usr/bin/env python3
"""
Meta Marketing API 클라이언트 모듈

Usage:
    from utils.meta_ads_client import MetaAdsClient
    client = MetaAdsClient()
    campaigns = client.list_campaigns()
"""

import os
from pathlib import Path
from typing import Any

import requests
from facebook_business.adobjects.ad import Ad
from facebook_business.adobjects.adaccount import AdAccount
from facebook_business.adobjects.adcreative import AdCreative
from facebook_business.adobjects.adimage import AdImage
from facebook_business.adobjects.adset import AdSet
from facebook_business.adobjects.campaign import Campaign
from facebook_business.api import FacebookAdsApi
from facebook_business.exceptions import FacebookRequestError

from utils.env_loader import load_env_keys  # type: ignore[import-not-found]
from utils.logger import get_logger  # type: ignore[import-not-found]

logger = get_logger(__name__)

_GRAPH_API_BASE = "https://graph.facebook.com/v25.0"
_ENV_KEYS_PATH = "/home/jay/workspace/.env.keys"

_DEFAULT_INSIGHT_FIELDS = [
    "impressions",
    "clicks",
    "spend",
    "cpc",
    "cpm",
    "ctr",
    "actions",
]


class MetaAdsClient:
    """Meta Marketing API 클라이언트"""

    def __init__(self, env_keys_path: str = _ENV_KEYS_PATH) -> None:
        """
        MetaAdsClient 초기화.

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

        Raises:
            ValueError: 필수 환경변수가 누락된 경우
        """
        self._env_keys_path = env_keys_path
        load_env_keys(env_keys_path)

        required_keys = [
            "META_APP_ID",
            "META_APP_SECRET",
            "META_ACCESS_TOKEN",
            "META_AD_ACCOUNT_ID",
        ]
        missing = [k for k in required_keys if not os.environ.get(k)]
        if missing:
            raise ValueError(f"필수 환경변수 누락: {missing}")

        self._app_id = os.environ["META_APP_ID"]
        self._app_secret = os.environ["META_APP_SECRET"]
        self._access_token = os.environ["META_ACCESS_TOKEN"]
        self._ad_account_id = os.environ["META_AD_ACCOUNT_ID"]

        self._init_api(self._app_id, self._app_secret, self._access_token)
        self._account = AdAccount(self._ad_account_id)

        logger.info("MetaAdsClient 초기화 완료 (account_id=%s)", self._ad_account_id)

    # ------------------------------------------------------------------
    # 내부 헬퍼
    # ------------------------------------------------------------------

    def _init_api(self, app_id: str, app_secret: str, access_token: str) -> None:
        """FacebookAdsApi 초기화."""
        FacebookAdsApi.init(app_id, app_secret, access_token)
        logger.debug("FacebookAdsApi 초기화 완료")

    @staticmethod
    def _to_dict(obj: Any) -> dict:
        """SDK 반환 객체를 순수 dict로 변환."""
        if hasattr(obj, "export_all_data"):
            return obj.export_all_data()
        return dict(obj)

    # ------------------------------------------------------------------
    # 토큰 관리
    # ------------------------------------------------------------------

    def exchange_token(self) -> str:
        """
        단기 토큰을 장기 토큰(60일)으로 교환한다.

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

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

        Raises:
            requests.HTTPError: API 호출 실패 시
            ValueError: 응답에 access_token이 없을 경우
        """
        url = f"{_GRAPH_API_BASE}/oauth/access_token"
        params = {
            "grant_type": "fb_exchange_token",
            "client_id": self._app_id,
            "client_secret": self._app_secret,
            "fb_exchange_token": self._access_token,
        }
        logger.debug("장기 토큰 교환 요청 중")
        resp = requests.get(url, params=params, timeout=30)
        resp.raise_for_status()
        data = resp.json()

        if "access_token" not in data:
            raise ValueError(f"토큰 교환 응답에 access_token 없음: {data.get('error')}")

        new_token = data["access_token"]
        self._access_token = new_token
        os.environ["META_ACCESS_TOKEN"] = new_token
        self._init_api(self._app_id, self._app_secret, new_token)

        logger.info("장기 토큰 교환 성공 (token_type=%s)", data.get("token_type"))
        return new_token

    def update_env_token(self, new_token: str) -> None:
        """
        .env.keys 파일의 META_ACCESS_TOKEN 값을 업데이트한다.

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

        Args:
            new_token: 교체할 새 액세스 토큰 문자열
        """
        path = Path(self._env_keys_path)
        if not path.exists():
            logger.warning(".env.keys 파일이 없습니다: %s", path)
            return

        lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
        updated = False
        new_lines = []
        for line in lines:
            stripped = line.strip()
            prefix_export = "export META_ACCESS_TOKEN="
            prefix_plain = "META_ACCESS_TOKEN="
            if stripped.startswith(prefix_export):
                new_lines.append(f"export META_ACCESS_TOKEN={new_token}\n")
                updated = True
            elif stripped.startswith(prefix_plain) and not stripped.startswith("export"):
                new_lines.append(f"META_ACCESS_TOKEN={new_token}\n")
                updated = True
            else:
                new_lines.append(line)

        if not updated:
            new_lines.append(f"\nexport META_ACCESS_TOKEN={new_token}\n")
            logger.warning("META_ACCESS_TOKEN 라인이 없어 파일 끝에 추가했습니다")

        path.write_text("".join(new_lines), encoding="utf-8")
        os.environ["META_ACCESS_TOKEN"] = new_token
        self._access_token = new_token
        logger.info(".env.keys 파일의 META_ACCESS_TOKEN 업데이트 완료")

    def check_token(self) -> dict:
        """
        현재 액세스 토큰의 유효성을 확인한다.

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

        Returns:
            dict: Graph API debug_token 응답의 data 필드
        """
        url = f"{_GRAPH_API_BASE}/debug_token"
        params = {
            "input_token": self._access_token,
            "access_token": f"{self._app_id}|{self._app_secret}",
        }
        logger.debug("토큰 유효성 확인 중")
        resp = requests.get(url, params=params, timeout=30)
        resp.raise_for_status()
        result = resp.json()
        data = result.get("data", result)
        logger.debug("토큰 유효성: is_valid=%s", data.get("is_valid"))
        return data

    # ------------------------------------------------------------------
    # 계정 정보
    # ------------------------------------------------------------------

    def get_account_info(self) -> dict:
        """
        광고 계정 상태, 잔액, 스펜딩 한도 정보를 반환한다.

        Returns:
            dict: account_status, balance, spend_cap, amount_spent,
                  currency, timezone_name, name 포함 dict
        """
        fields = [
            AdAccount.Field.name,
            AdAccount.Field.account_status,
            AdAccount.Field.amount_spent,
            AdAccount.Field.balance,
            AdAccount.Field.spend_cap,
            AdAccount.Field.currency,
            AdAccount.Field.timezone_name,
        ]
        try:
            data = self._account.api_get(fields=fields)
            logger.debug("계정 정보 조회 완료")
            return self._to_dict(data)
        except FacebookRequestError as e:
            logger.error("계정 정보 조회 실패: %s", e)
            raise

    # ------------------------------------------------------------------
    # 캠페인 CRUD
    # ------------------------------------------------------------------

    def list_campaigns(
        self,
        fields: list[str] | None = None,
        limit: int = 25,
    ) -> list[dict]:
        """
        광고 계정의 캠페인 목록을 반환한다.

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

        Returns:
            list[dict]: 캠페인 정보 목록
        """
        if fields is None:
            fields = [
                Campaign.Field.name,
                Campaign.Field.status,
                Campaign.Field.objective,
                Campaign.Field.daily_budget,
                Campaign.Field.lifetime_budget,
                Campaign.Field.created_time,
                Campaign.Field.updated_time,
            ]
        try:
            campaigns = self._account.get_campaigns(
                fields=fields,
                params={"limit": limit},
            )
            result = [self._to_dict(c) for c in list(campaigns)]  # type: ignore[reportArgumentType]
            logger.debug("캠페인 목록 조회 완료: %d건", len(result))
            return result
        except FacebookRequestError as e:
            logger.error("캠페인 목록 조회 실패: %s", e)
            raise

    def get_campaign(
        self,
        campaign_id: str,
        fields: list[str] | None = None,
    ) -> dict:
        """
        단일 캠페인 정보를 반환한다.

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

        Returns:
            dict: 캠페인 정보
        """
        if fields is None:
            fields = [
                Campaign.Field.name,
                Campaign.Field.status,
                Campaign.Field.objective,
                Campaign.Field.daily_budget,
                Campaign.Field.lifetime_budget,
                Campaign.Field.created_time,
                Campaign.Field.updated_time,
            ]
        try:
            campaign = Campaign(campaign_id).api_get(fields=fields)
            result = self._to_dict(campaign)
            logger.debug("캠페인 조회 완료: id=%s", campaign_id)
            return result
        except FacebookRequestError as e:
            logger.error("캠페인 조회 실패 (id=%s): %s", campaign_id, e)
            raise

    def create_campaign(
        self,
        name: str,
        objective: str,
        status: str = "PAUSED",
        daily_budget: int | None = None,
        special_ad_categories: list[str] | None = None,
    ) -> dict:
        """
        새 캠페인을 생성한다.

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

        Returns:
            dict: 생성된 캠페인 정보 (id, name 등)
        """
        params: dict[str, Any] = {
            Campaign.Field.name: name,
            Campaign.Field.objective: objective,
            Campaign.Field.status: status,
            "special_ad_categories": special_ad_categories if special_ad_categories is not None else [],
        }
        if daily_budget is not None:
            params[Campaign.Field.daily_budget] = daily_budget

        try:
            campaign = self._account.create_campaign(fields=[], params=params)
            result = self._to_dict(campaign)
            logger.info("캠페인 생성 완료: id=%s name=%s", result.get("id"), name)
            return result
        except FacebookRequestError as e:
            logger.error("캠페인 생성 실패: %s", e)
            raise

    def update_campaign(self, campaign_id: str, **params: Any) -> dict:
        """
        기존 캠페인을 업데이트한다.

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

        Returns:
            dict: API 응답
        """
        try:
            campaign = Campaign(campaign_id)
            result = campaign.api_update(params=params)
            logger.info("캠페인 업데이트 완료: id=%s fields=%s", campaign_id, list(params.keys()))
            return self._to_dict(result)
        except FacebookRequestError as e:
            logger.error("캠페인 업데이트 실패 (id=%s): %s", campaign_id, e)
            raise

    def delete_campaign(self, campaign_id: str) -> bool:
        """
        캠페인을 삭제한다 (상태를 DELETED로 설정).

        Args:
            campaign_id: 삭제할 캠페인 ID

        Returns:
            bool: 삭제 성공 여부
        """
        try:
            Campaign(campaign_id).api_delete()
            logger.info("캠페인 삭제 완료: id=%s", campaign_id)
            return True
        except FacebookRequestError as e:
            logger.error("캠페인 삭제 실패 (id=%s): %s", campaign_id, e)
            raise

    # ------------------------------------------------------------------
    # 광고세트 CRUD
    # ------------------------------------------------------------------

    def list_adsets(
        self,
        campaign_id: str | None = None,
        fields: list[str] | None = None,
        limit: int = 25,
    ) -> list[dict]:
        """
        광고세트 목록을 반환한다.

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

        Returns:
            list[dict]: 광고세트 정보 목록
        """
        if fields is None:
            fields = [
                AdSet.Field.name,
                AdSet.Field.status,
                AdSet.Field.daily_budget,
                AdSet.Field.targeting,
                AdSet.Field.optimization_goal,
                AdSet.Field.billing_event,
                AdSet.Field.start_time,
                AdSet.Field.end_time,
            ]
        params = {"limit": limit}
        try:
            if campaign_id:
                adsets = Campaign(campaign_id).get_ad_sets(fields=fields, params=params)
            else:
                adsets = self._account.get_ad_sets(fields=fields, params=params)
            result = [self._to_dict(a) for a in list(adsets)]  # type: ignore[reportArgumentType]
            logger.debug("광고세트 목록 조회 완료: %d건", len(result))
            return result
        except FacebookRequestError as e:
            logger.error("광고세트 목록 조회 실패: %s", e)
            raise

    def create_adset(
        self,
        campaign_id: str,
        name: str,
        daily_budget: int,
        targeting: dict,
        optimization_goal: str,
        billing_event: str,
        status: str = "PAUSED",
    ) -> dict:
        """
        광고세트를 생성한다.

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

        Returns:
            dict: 생성된 광고세트 정보
        """
        params: dict[str, Any] = {
            AdSet.Field.name: name,
            AdSet.Field.campaign_id: campaign_id,
            AdSet.Field.daily_budget: daily_budget,
            AdSet.Field.targeting: targeting,
            AdSet.Field.optimization_goal: optimization_goal,
            AdSet.Field.billing_event: billing_event,
            AdSet.Field.status: status,
        }
        try:
            adset = self._account.create_ad_set(fields=[], params=params)
            result = self._to_dict(adset)
            logger.info("광고세트 생성 완료: id=%s name=%s", result.get("id"), name)
            return result
        except FacebookRequestError as e:
            logger.error("광고세트 생성 실패: %s", e)
            raise

    def update_adset(self, adset_id: str, **params: Any) -> dict:
        """
        기존 광고세트를 업데이트한다.

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

        Returns:
            dict: API 응답
        """
        try:
            adset = AdSet(adset_id)
            result = adset.api_update(params=params)
            logger.info("광고세트 업데이트 완료: id=%s fields=%s", adset_id, list(params.keys()))
            return self._to_dict(result)
        except FacebookRequestError as e:
            logger.error("광고세트 업데이트 실패 (id=%s): %s", adset_id, e)
            raise

    def delete_adset(self, adset_id: str) -> bool:
        """
        광고세트를 삭제한다.

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

        Returns:
            bool: 삭제 성공 여부
        """
        try:
            AdSet(adset_id).api_delete()
            logger.info("광고세트 삭제 완료: id=%s", adset_id)
            return True
        except FacebookRequestError as e:
            logger.error("광고세트 삭제 실패 (id=%s): %s", adset_id, e)
            raise

    # ------------------------------------------------------------------
    # 크리에이티브 관리
    # ------------------------------------------------------------------

    def upload_image(self, image_path: str) -> str:
        """
        이미지 파일을 Meta 광고 이미지로 업로드하고 hash를 반환한다.

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

        Returns:
            str: 업로드된 이미지의 hash 값
        """
        path = Path(image_path)
        if not path.exists():
            raise FileNotFoundError(f"이미지 파일을 찾을 수 없습니다: {image_path}")

        params = {AdImage.Field.filename: str(path)}
        try:
            image = self._account.create_ad_image(fields=[], params=params)
            data = self._to_dict(image)
            # images 키 아래에 파일명 키로 결과가 중첩될 수 있음
            if "images" in data:
                inner = data["images"]
                first_key = next(iter(inner))
                data = inner[first_key]
            image_hash = data.get("hash", "")
            logger.info("이미지 업로드 완료: %s (hash=%s)", path.name, image_hash)
            return image_hash
        except FacebookRequestError as e:
            logger.error("이미지 업로드 실패 (%s): %s", path.name, e)
            raise

    def create_creative(
        self,
        name: str,
        image_hash: str,
        page_id: str,
        message: str,
        link: str | None = None,
    ) -> dict:
        """
        광고 크리에이티브를 생성한다.

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

        Returns:
            dict: 생성된 AdCreative 정보
        """
        link_data: dict[str, Any] = {
            "message": message,
            "image_hash": image_hash,
        }
        if link:
            link_data["link"] = link

        object_story_spec = {
            "page_id": page_id,
            "link_data": link_data,
        }

        params: dict[str, Any] = {
            AdCreative.Field.name: name,
            AdCreative.Field.object_story_spec: object_story_spec,
        }
        try:
            creative = self._account.create_ad_creative(fields=[], params=params)
            result = self._to_dict(creative)
            logger.info("광고 크리에이티브 생성 완료: id=%s name=%s", result.get("id"), name)
            return result
        except FacebookRequestError as e:
            logger.error("광고 크리에이티브 생성 실패: %s", e)
            raise

    # ------------------------------------------------------------------
    # 인사이트 조회
    # ------------------------------------------------------------------

    def get_insights(
        self,
        object_id: str,
        object_type: str = "campaign",
        fields: list[str] | None = None,
        date_preset: str | None = None,
        time_range: dict | None = None,
    ) -> list[dict]:
        """
        광고 성과 인사이트를 반환한다.

        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]: 인사이트 레코드 목록
        """
        if fields is None:
            fields = _DEFAULT_INSIGHT_FIELDS

        params: dict[str, Any] = {}
        if date_preset:
            params["date_preset"] = date_preset
        elif time_range:
            params["time_range"] = time_range
        else:
            params["date_preset"] = "last_7d"

        try:
            obj_type_lower = object_type.lower()
            if obj_type_lower == "campaign":
                target = Campaign(object_id)
            elif obj_type_lower == "adset":
                target = AdSet(object_id)
            elif obj_type_lower == "ad":
                target = Ad(object_id)
            else:
                raise ValueError(
                    f"지원하지 않는 object_type: {object_type}. 'campaign', 'adset', 'ad' 중 하나여야 합니다."
                )

            insights = target.get_insights(fields=fields, params=params)
            result = [self._to_dict(i) for i in list(insights)]  # type: ignore[reportArgumentType]
            logger.debug(
                "인사이트 조회 완료: %d건 (object_type=%s, id=%s)",
                len(result),
                object_type,
                object_id,
            )
            return result
        except FacebookRequestError as e:
            logger.error("인사이트 조회 실패 (object_type=%s, id=%s): %s", object_type, object_id, e)
            raise
