import os
import time
import re
from datetime import datetime, timedelta, timezone
from pathlib import Path
from dotenv import load_dotenv

from facebook_business.api import FacebookAdsApi
from facebook_business.adobjects.adaccount import AdAccount
from facebook_business.adobjects.campaign import Campaign
from facebook_business.adobjects.adset import AdSet

from killer import kill_ad, kill_adset, kill_campaign , try_kill_account
from duplicater import duplicate_ad, duplicate_adset, duplicate_campaign
from scorer import calc_score
from recommender import build_recommendations

from scaler import get_last_budget_change, scale_adset_budget, was_scaled_in_last_48h

# === Config ===
REPEATER_TIMER = 7200  # 2 hours

load_dotenv()
TOKEN, ACCOUNT_ID = os.getenv("FB_ACCESS_TOKEN"), os.getenv("AD_ACCOUNT_ID")
if not TOKEN or not ACCOUNT_ID:
    raise SystemExit("Missing FB_ACCESS_TOKEN or AD_ACCOUNT_ID")

FacebookAdsApi.init(access_token=TOKEN)

# === Helpers ===
INSIGHT_FIELDS = [
    "impressions", "reach", "frequency", "clicks", "inline_link_clicks",
    "ctr", "cpm", "cpc", "spend", "actions", "cost_per_action_type"
]

LEAD_KEYS = (
    "onsite_conversion.lead_grouped",
    "offsite_conversion.fb_pixel_lead",
    "lead",
    "leadgen.other",
)


def parse_datetime(ts: str, fallback: datetime) -> datetime:
    """Parse Facebook timestamp safely."""
    if not ts:
        return fallback
    if ts.endswith("+0000"):
        ts = ts[:-5] + "+00:00"
    return datetime.fromisoformat(ts)


def nf(x):
    """Convert safely to float."""
    try:
        return float(x or 0)
    except Exception:
        return 0.0


def get_kpi_data(adset_id, created_time, now):
    """Get insights and structured KPI data for an ad set."""
    ins_params = {
        "level": "adset",
        "action_report_time": "conversion",
        "action_attribution_windows": ["7d_click", "1d_view"],
        "time_range": {
            "since": created_time.strftime("%Y-%m-%d"),
            "until": (now + timedelta(days=1)).strftime("%Y-%m-%d"),
        },
    }
    kpi = (AdSet(adset_id).get_insights(fields=INSIGHT_FIELDS, params=ins_params) or [{}])[0]
    return kpi


def extract_metrics(kpi):
    """Extract all relevant metrics and results from a KPI response."""
    impressions = kpi.get("impressions")
    reach = kpi.get("reach")
    frequency = kpi.get("frequency")
    clicks = kpi.get("clicks")
    link_clicks = kpi.get("inline_link_clicks")
    ctr = kpi.get("ctr")
    cpm = kpi.get("cpm")
    cpc = kpi.get("cpc")
    spend = kpi.get("spend")

    actions_map = {a.get("action_type"): a.get("value") for a in (kpi.get("actions") or [])}
    cpa_map = {c.get("action_type"): c.get("value") for c in (kpi.get("cost_per_action_type") or [])}
    rk = next((k for k in LEAD_KEYS if k in actions_map), None)

    lpv = actions_map.get("landing_page_view", "0")
    leads = actions_map.get(rk, "0") if rk else "0"
    cpl = cpa_map.get(rk) if rk else None

    return impressions, reach, frequency, clicks, link_clicks, ctr, cpm, cpc, spend, lpv, leads, cpl


def get_budget_eur(s):
    """Extract daily or lifetime budget in EUR."""
    b = s.get("daily_budget") or s.get("lifetime_budget")
    try:
        return int(b) / 100 if b else 0.0
    except Exception:
        return 0.0

def last_edit(adset_id, now_dt):
    """Return how many days ago the ad set was last edited."""
    try:
        acts = list(AdSet(adset_id).get_activities(params={"limit": 50}))
        if not acts:
            return None  # no recorded edits

        # find the most recent edit timestamp
        latest_ts = max(a.get("event_time") for a in acts if a.get("event_time"))
        if latest_ts.endswith("+0000"):
            latest_ts = latest_ts[:-5] + "+00:00"
        last_edit_dt = datetime.fromisoformat(latest_ts)

        # compute how many days ago it happened
        return (now_dt.date() - last_edit_dt.date()).days
    except Exception:
        return None

# === Main ===
def run_once():
    now = datetime.now(timezone.utc)

    outdir = Path(__file__).parent / "reports_2025"
    outdir.mkdir(parents=True, exist_ok=True)
    REPORT_FILE = outdir / f"{now:%Y-%m-%d_%H-%M-%S}.txt" 

    report = [f"\n== {now:%Y-%m-%d %H:%M:%S} =="]

    report.append(str(try_kill_account(ACCOUNT_ID))+"\n")
    report.append("*" * 90 + "\n")

    campaigns = AdAccount(ACCOUNT_ID).get_campaigns(
        fields=["id", "name"],
        params={"limit": 200, "effective_status": ["ACTIVE"]},
    )

    for camp in campaigns:
        cid, cname = camp["id"], camp.get("name")
        adsets = list(
            Campaign(cid).get_ad_sets(
                fields=["id", "name", "created_time", "daily_budget", "lifetime_budget"],
                params={"limit": 200, "effective_status": ["ACTIVE"]},
            )
        )
        n = len(adsets)

        # --- case 1: no active ad sets ---
        if n == 0:
            report.append(
                f"Case .1. null Active Ad Sets.\n"
                f"Campaign {cid} | {cname}: \n=======> KI-System has killed all the ad sets, campaign is over, start a new one.\n\n"
                + "*" * 90 + "\n"
            )
            kill_campaign(cid)
            continue

        # --- case 2: exactly one ad set ---
        if n == 1:
            s = adsets[0]
            sname = s.get("name")

            m = re.search(r"TCPL\s*=\s*(\d+)", sname)
            target_cpl = float(m.group(1)) if m else 60.0
            
            created_utc = parse_datetime(s.get("created_time"), now)
            age = (now.date() - created_utc.date()).days

            kpi = get_kpi_data(s["id"], created_utc, now)
            (
                impressions, reach, frequency, clicks, link_clicks,
                ctr, cpm, cpc, spend, lpv, leads, cpl
            ) = extract_metrics(kpi)

            budget_eur = get_budget_eur(s)
            score = calc_score(
                impressions, reach, frequency, clicks, link_clicks, lpv,
                ctr, cpm, cpc, cpl, budget_eur, spend, leads, target_cpl
            )
            recommendations = build_recommendations(
                impressions, reach, frequency, clicks, link_clicks, lpv,
                ctr, cpm, cpc, cpl, budget_eur, spend, leads
            )

            scaled_last_48h = was_scaled_in_last_48h(s['id'])
            last_budget_change = get_last_budget_change(s['id'])
            days_since_edit = last_edit(s['id'], now) or 999

            info = (
                f"Info:"
                f"\n - Campaign {cid} | {cname}: / Ad Set: {sname} ({s['id']})"
                f"\n - Age: {age} days | Budget: €{budget_eur:.2f}"
                f"\n - Impressions: {impressions or 0} | Reach: {reach or 0} | Frequency: {frequency or 0}"
                f"\n - Clicks: {clicks or 0} | Link Clicks: {link_clicks or 0} | Landing Page Views: {lpv or 0}"
                f"\n - CTR: {ctr or 0} | CPM: {cpm or 0} | CPC: {cpc or 0} | CPL: {cpl or 0} | Target CPL: {target_cpl}"
                f"\n - Leads: {leads or 0} | Spend: €{spend or 0}"
                f"\n - Last Budget Change: {last_budget_change or 0}"
                f"\n - last edited: {last_edit(s['id'], now) or 0} days ago"
            )

            report.append(
                f"Case .2. One Active Ad Set.\n{info}\n{recommendations}\n"
            )

            if age >= 3 and score < 6:
              #  duplicate_adset(s["id"],budget=target_cpl)
                kill_adset(s["id"])
                report.append(
                    f"=======> System Decided To Kill The Ad Set Due To Bad Performance — Score: {score}.\n" + "*" * 90 + "\n"
                #    f"=======> But Duplicated The Adset, with New Budget/Target CPL: {target_cpl}.\n\n" + "*" * 90 + "\n"
                )
            # elif age >= 5 and score >= 12 and score < 13.5 and not scaled_last_48h and days_since_edit > 2 and budget_eur < 150:
            #  #   scale_adset_budget(s["id"])
            #     report.append(
            #         f"=======> System Decided To Scale The Ad Set Due To Good Performance — Score: {score}.\n\n" + "*" * 90 + "\n"
            #     )
            # elif age >= 7 and score >= 13.5 and days_since_edit > 2 and not scaled_last_48h:
            #   #  duplicate_adset(s["id"],budget=target_cpl)
            #     report.append(
            #         f"=======> System Decided To Duplicate The Ad Set Due To Excellent Performance — Score: {score}.\n\n" + "*" * 90 + "\n"
            #     )
            else:
                report.append(
                    f"=======> System Decided To Keep The Ad Set Due To Stable Performance — Score: {score}.\n\n" + "*" * 90 + "\n"
                )
            continue

        # --- case 3: multiple ad sets ---
        rows = []
        for s in adsets:
            sname = s.get("name")

            m = re.search(r"TCPL\s*=\s*(\d+)", sname)
            target_cpl = float(m.group(1)) if m else 60.0

            created = parse_datetime(s.get("created_time"), now)
            age = (now.date() - created.date()).days

            kpi = get_kpi_data(s["id"], created, now)
            (
                impressions, reach, frequency, clicks, link_clicks,
                ctr, cpm, cpc, spend, lpv, leads, cpl
            ) = extract_metrics(kpi)

            budget_eur = get_budget_eur(s)
            score = calc_score(
                impressions, reach, frequency, clicks, link_clicks, lpv,
                ctr, cpm, cpc, cpl, budget_eur, spend, leads, target_cpl
            )

            rows.append({
                "id": s["id"], "name": sname, "age": age, "score": score,
                "impressions": impressions, "reach": reach, "frequency": frequency,
                "clicks": clicks, "link_clicks": link_clicks, "lpv": lpv,
                "ctr": ctr, "cpm": cpm, "cpc": cpc, "spend": spend,
                "leads": leads, "target_cpl": target_cpl, "cpl": cpl, "budget_eur": budget_eur
            })

        def rank_key(r):
            return (-nf(r["score"]), nf(r["cpl"]), -nf(r["leads"]), -nf(r["ctr"]),
                    nf(r["cpc"]), nf(r["cpm"]), nf(r["frequency"]))

        rows.sort(key=rank_key)
        winner, losers = rows[0], rows[1:]

        report.append(f"Case .3. Multiple Active Ad Sets.\n")

        scaled_last_48h = was_scaled_in_last_48h(winner['id'])

        report.append(f" - System Detected The WINNER: {winner['name']} ({winner['id']}) | Score: {winner['score']}")
        
        days_since_edit = last_edit(winner['id'], now) or 999

        # if winner["age"] >= 5 and winner["age"] < 7 and winner["score"] > 8 and winner["budget_eur"] < 150 and days_since_edit >= 2:
        #     scale_adset_budget(winner['id'])
        # if winner["age"] >= 7 and winner["score"] > 12 and winner["budget_eur"] < 150 and days_since_edit >= 2:
        #     scale_adset_budget(winner['id'])

        for r in losers:
            if r["age"] < 3 or r["score"] > 9:
                report.append(f" - System SKIPPED Due To (new/edited ≤3d oder es ist noch gut): {r['name']} ({r['id']}) | Score: {r['score']}")
                continue
          #  kill_adset(r["id"])
            report.append(f" - System KILLED: {r['name']} ({r['id']}) | Score: {r['score']}")

        report.append("\n" + "*" * 90 + "\n")

    # --- write report ---
    with open(REPORT_FILE, "a", encoding="utf-8") as f:
        f.write("\n".join(report) + "\n")
    report.clear()

# === Repeat loop ===
if __name__ == "__main__":
    while True:
        try:
            run_once()
            print("Report written at", datetime.now().strftime("%H:%M:%S"))
        except Exception as e:
            print("⚠️ Error:", e)
        time.sleep(REPEATER_TIMER)
