from __future__ import annotations

import csv
import json
import os
import re
from collections import defaultdict
from collections import Counter
from dataclasses import dataclass
from pathlib import Path

import requests
from google.protobuf import field_mask_pb2

from ..config import ClientConfig, client_dir
from ..google_ads import get_google_ads_client, run_query
from ..history import append_change_markdown, append_history, now_local


CSV_COLS = [
    "id", "offer_id", "title", "availability", "channel", "content_language",
    "target_country", "feed_label", "brand", "google_product_category",
    "custom_label_0", "custom_label_1", "custom_label_2", "custom_label_3",
    "custom_label_4", "link",
]

CATCH_ALL_VARIANT = "catch_all"


@dataclass
class SyncPlan:
    campaigns: list[dict]
    groups_total: int
    groups_with_product_id: int
    create_plan: list[dict]
    enable_plan: list[dict]
    pause_plan: list[dict]
    rename_plan: list[dict]
    warnings: list[str]
    unmatched_groups: list[dict] | None = None

    def to_dict(self) -> dict:
        def serialize_rows(rows: list[dict]) -> list[dict]:
            serialized = []
            for item in rows:
                row = {}
                for key, value in item.items():
                    if isinstance(value, set):
                        row[key] = sorted(value)
                    else:
                        row[key] = value
                serialized.append(row)
            return serialized

        return {
            "task": "sync_pla_cl1",
            "campaigns": serialize_rows(self.campaigns),
            "groups_total": self.groups_total,
            "groups_with_product_id": self.groups_with_product_id,
            "create_plan": serialize_rows(self.create_plan),
            "enable_plan": serialize_rows(self.enable_plan),
            "pause_plan": serialize_rows(self.pause_plan),
            "rename_plan": serialize_rows(self.rename_plan),
            "warnings": self.warnings,
            "unmatched_groups": serialize_rows(self.unmatched_groups or []),
        }

    @classmethod
    def from_dict(cls, data: dict) -> "SyncPlan":
        return cls(
            campaigns=data.get("campaigns", []),
            groups_total=int(data.get("groups_total", 0)),
            groups_with_product_id=int(data.get("groups_with_product_id", 0)),
            create_plan=data.get("create_plan", []),
            enable_plan=data.get("enable_plan", []),
            pause_plan=data.get("pause_plan", []),
            rename_plan=data.get("rename_plan", []),
            warnings=data.get("warnings", []),
            unmatched_groups=data.get("unmatched_groups", []),
        )


def campaign_action_summary(plan: SyncPlan) -> list[dict]:
    campaign_names = set()
    for action_name in ("create_plan", "enable_plan", "pause_plan", "rename_plan"):
        for row in getattr(plan, action_name):
            if row.get("campaign_name"):
                campaign_names.add(row["campaign_name"])

    create_counts = Counter(row["campaign_name"] for row in plan.create_plan)
    enable_counts = Counter(row["campaign_name"] for row in plan.enable_plan)
    pause_counts = Counter(row["campaign_name"] for row in plan.pause_plan)
    rename_counts = Counter(row["campaign_name"] for row in plan.rename_plan)

    return [
        {
            "campaign_name": name,
            "create": create_counts.get(name, 0),
            "enable": enable_counts.get(name, 0),
            "pause": pause_counts.get(name, 0),
            "rename": rename_counts.get(name, 0),
        }
        for name in sorted(campaign_names)
    ]


def normalize_text(value: str) -> str:
    return " ".join(
        (value or "")
        .lower()
        .replace("–", "-")
        .replace("—", "-")
        .replace("|", "-")
        .replace("„", "")
        .replace("”", "")
        .replace('"', "")
        .split()
    )


def ad_group_name_for_product(product: dict, duplicate_title: bool = False) -> str:
    title = (product.get("title") or "").strip()
    offer_id = (product.get("offer_id") or "").strip()
    if not duplicate_title or not offer_id:
        return title
    suffix = f" | {offer_id}"
    return f"{title[:255 - len(suffix)]}{suffix}"


def parse_allowed_labels(campaign_name: str) -> set[str]:
    match = re.search(r"\]\s*(.+)$", campaign_name)
    raw = match.group(1).strip() if match else campaign_name
    if "|" in raw:
        raw = raw.split("|", 1)[0].strip()
    return {part.strip() for part in raw.split(",") if part.strip()}


def normalize_variant(value: str) -> str:
    return (value or "").strip().lower()


def product_has_paused_cl4(product: dict) -> bool:
    return normalize_variant(product.get("custom_label_4")) == "paused"


def parse_campaign_variant(campaign_name: str) -> str:
    """Wariant kampanii z czesci po znaku '|' w nazwie, np. 'catch_all'.

    Kampania bez '|' ma wariant pusty i odpowiada produktom z pustym CL4.
    """
    match = re.search(r"\]\s*(.+)$", campaign_name)
    raw = match.group(1).strip() if match else campaign_name
    if "|" in raw:
        return normalize_variant(raw.split("|", 1)[1])
    return ""


def is_shared_catch_all_campaign(campaign_name: str) -> bool:
    match = re.search(r"\]\s*(.+)$", campaign_name)
    raw = match.group(1).strip() if match else campaign_name
    return "|" not in raw and normalize_variant(raw) == CATCH_ALL_VARIANT


def campaign_record_from_row(row) -> dict:
    shared_catch_all = is_shared_catch_all_campaign(row.campaign.name)
    return {
        "id": str(row.campaign.id),
        "name": row.campaign.name,
        "status": row.campaign.status.name,
        "allowed": set() if shared_catch_all else parse_allowed_labels(row.campaign.name),
        "variant": CATCH_ALL_VARIANT if shared_catch_all else parse_campaign_variant(row.campaign.name),
        "shared_catch_all": shared_catch_all,
    }


def fetch_adspro_products(client: ClientConfig, segments: list[str]) -> list[dict]:
    api_url = os.environ.get("ADSPRO_API_URL")
    api_key = os.environ.get("ADSPRO_API_KEY")
    if not api_url or not api_key:
        raise RuntimeError("Brak ADSPRO_API_URL lub ADSPRO_API_KEY w .env.")
    if not client.adspro_client_id:
        raise RuntimeError(f"Brak adspro_client_id dla {client.domain} w config/clients.toml.")

    by_offer_id = {}
    for segment in segments:
        response = requests.post(
            api_url,
            data={
                "action": "products_get_by_cl1",
                "api_key": api_key,
                "client_id": client.adspro_client_id,
                "custom_label_1": segment,
            },
            timeout=60,
        )
        data = response.json()
        if data.get("result") == "error":
            raise RuntimeError(f"adsPRO zwrocil blad dla CL1={segment}: {data.get('message')}")
        for product in data.get("products", []):
            offer_id = product.get("offer_id") or ""
            if offer_id:
                by_offer_id[offer_id] = {
                    "id": "",
                    "offer_id": offer_id,
                    "title": product.get("title", "") or "",
                    "availability": "",
                    "channel": "",
                    "content_language": "",
                    "target_country": "",
                    "feed_label": "",
                    "brand": "",
                    "google_product_category": product.get("google_product_category", "") or "",
                    "custom_label_0": "",
                    "custom_label_1": product.get("custom_label_1", "") or "",
                    "custom_label_2": "",
                    "custom_label_3": product.get("custom_label_3", "") or "",
                    "custom_label_4": product.get("custom_label_4", "") or "",
                    "link": "",
                }
    return list(by_offer_id.values())


def save_products_csv(domain: str, products: list[dict]) -> Path:
    out = client_dir(domain) / "data"
    out.mkdir(parents=True, exist_ok=True)
    path = out / "merchant_produkty_adspro.csv"
    with path.open("w", newline="", encoding="utf-8-sig") as f:
        writer = csv.DictWriter(f, fieldnames=CSV_COLS)
        writer.writeheader()
        writer.writerows(products)
    return path


def save_plan_files(domain: str, plan: SyncPlan, products_count: int) -> tuple[Path, Path]:
    ts = now_local()
    base = client_dir(domain) / "plans"
    base.mkdir(parents=True, exist_ok=True)
    stem = f"{ts.strftime('%Y-%m-%d_%H-%M-%S')}_sync_pla_cl1"
    json_path = base / f"{stem}.json"
    md_path = base / f"{stem}.md"

    payload = {
        "created_at": ts.isoformat(timespec="seconds"),
        "client": domain,
        "products_count": products_count,
        **plan.to_dict(),
    }
    json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")

    lines = [
        "# Plan: Synchronizacja kampanii PLA_CL1",
        "",
        f"Klient: {domain}",
        f"Utworzono: {ts.isoformat(timespec='seconds')}",
        "",
        "## Podsumowanie",
        "",
        f"- Kampanie PLA_CL1: {len(plan.campaigns)}",
        f"- Produkty z adsPRO: {products_count}",
        f"- Grupy reklam obecnie: {plan.groups_total}",
        f"- Grupy reklam z identyfikatorem produktu: {plan.groups_with_product_id}",
        f"- Do utworzenia: {len(plan.create_plan)}",
        f"- Do włączenia: {len(plan.enable_plan)}",
        f"- Do wstrzymania: {len(plan.pause_plan)}",
        f"- Do zmiany nazwy: {len(plan.rename_plan)}",
        "",
    ]
    if plan.warnings:
        lines.extend(["## Uwagi", ""])
        lines.extend(f"- {warning}" for warning in plan.warnings)
        lines.append("")
    summary = campaign_action_summary(plan)
    if summary:
        lines.extend(["## Podsumowanie po kampaniach", "", "| Kampania | Utworz | Wlacz | Wstrzymaj | Zmien nazwe |", "| --- | ---: | ---: | ---: | ---: |"])
        for row in summary:
            lines.append(
                f"| {row['campaign_name']} | {row['create']} | {row['enable']} | {row['pause']} | {row['rename']} |"
            )
        lines.append("")
    if plan.unmatched_groups:
        lines.extend(["## Grupy reklam bez dopasowania w adsPRO", "", "| Kampania | Grupa reklam | Status | Identyfikator produktu |", "| --- | --- | --- | --- |"])
        for row in plan.unmatched_groups:
            lines.append(
                f"| {row['campaign_name']} | {row['ad_group_name']} | {row['ad_group_status']} | {row.get('offer_id', '')} |"
            )
        lines.append("")
    if plan.create_plan:
        lines.extend(["## Grupy reklam do utworzenia", "", "| Kampania | Grupa reklam | Produkt | Powod |", "| --- | --- | --- | --- |"])
        for row in plan.create_plan:
            lines.append(
                f"| {row['campaign_name']} | {row['ad_group_name']} | {row['product_id']} | {row['reason']} |"
            )
        lines.append("")
    if plan.enable_plan:
        lines.extend(["## Grupy reklam do wlaczenia", "", "| Kampania | Grupa reklam | Produkt | Powod |", "| --- | --- | --- | --- |"])
        for row in plan.enable_plan:
            lines.append(
                f"| {row['campaign_name']} | {row['ad_group_name']} | {row['product_id']} | {row['reason']} |"
            )
        lines.append("")
    if plan.pause_plan:
        lines.extend(["## Grupy reklam do wstrzymania", "", "| Kampania | Grupa reklam | Powod |", "| --- | --- | --- |"])
        for row in plan.pause_plan:
            lines.append(f"| {row['campaign_name']} | {row['ad_group_name']} | {row['reason']} |")
        lines.append("")
    if plan.rename_plan:
        lines.extend(["## Nazwy grup reklam do zmiany", "", "| Kampania | Obecna nazwa | Nowa nazwa |", "| --- | --- | --- |"])
        for row in plan.rename_plan:
            lines.append(f"| {row['campaign_name']} | {row['old_name']} | {row['new_name']} |")
        lines.append("")
    md_path.write_text("\n".join(lines), encoding="utf-8")
    return json_path, md_path


def build_plan(client, customer_id: str, products: list[dict]) -> SyncPlan:
    campaign_rows = run_query(
        client,
        customer_id,
        """
        SELECT campaign.id, campaign.name, campaign.status
        FROM campaign
        WHERE campaign.name LIKE '%PLA_CL1%'
          AND campaign.status = 'ENABLED'
        """,
    )
    campaigns = [campaign_record_from_row(row) for row in campaign_rows]
    if not campaigns:
        return SyncPlan([], 0, 0, [], [], [], [], ["Nie znaleziono kampanii [PLA_CL1]."], [])

    # Kazda standardowa kampania celuje w pare (CL1, CL4): CL1 z czesci
    # przed '|', CL4 z czesci po '|'. Kampania "[PLA_CL1] catch_all" jest
    # wspolnym celem dla CL4=catch_all, gdy nie ma dedykowanej kampanii CL1.
    campaign_by_target = {}
    shared_catch_all_campaign = None
    variant_warnings = []
    for campaign in campaigns:
        if campaign.get("shared_catch_all"):
            if shared_catch_all_campaign:
                variant_warnings.append("Wiele wspolnych kampanii [PLA_CL1] catch_all.")
            shared_catch_all_campaign = campaign
            continue
        for label in campaign["allowed"]:
            key = (label, campaign["variant"])
            if key in campaign_by_target:
                variant_warnings.append(
                    f"Wiele kampanii dla tej samej pary CL1/CL4: "
                    f"{label} | {campaign['variant'] or '(bez wariantu)'}."
                )
            campaign_by_target[key] = campaign

    def resolve_target_campaign(product: dict):
        """Zwraca (kampania, typ_fallbacku) dla produktu wg jego CL1 i CL4."""
        label = (product.get("custom_label_1") or "").strip()
        variant = normalize_variant(product.get("custom_label_4"))
        if not label:
            return None, ""
        exact = campaign_by_target.get((label, variant))
        if exact:
            return exact, ""
        if (
            variant == CATCH_ALL_VARIANT
            and shared_catch_all_campaign
            and (label, "") in campaign_by_target
        ):
            return shared_catch_all_campaign, "shared_catch_all"
        if variant and (label, "") in campaign_by_target:
            return campaign_by_target[(label, "")], "base"
        return None, ""

    by_offer_id = {}
    by_title_norm = defaultdict(list)
    target_by_offer = {}
    products_by_campaign = defaultdict(list)
    base_fallback_offers = []
    shared_catch_all_offers = []
    orphan_offers = []
    paused_offers = []
    for product in products:
        offer_id = (product.get("offer_id") or "").strip()
        title = (product.get("title") or "").strip()
        label = (product.get("custom_label_1") or "").strip()
        if offer_id:
            by_offer_id[offer_id] = product
        if title:
            by_title_norm[normalize_text(title)].append(product)
        if not (label and title and offer_id):
            continue
        if product_has_paused_cl4(product):
            paused_offers.append(offer_id)
            continue
        target, fallback_type = resolve_target_campaign(product)
        if target is None:
            orphan_offers.append(offer_id)
            continue
        target_by_offer[offer_id] = target
        products_by_campaign[target["id"]].append(product)
        if fallback_type == "base":
            base_fallback_offers.append(offer_id)
        elif fallback_type == "shared_catch_all":
            shared_catch_all_offers.append(offer_id)

    campaign_ids = ", ".join(c["id"] for c in campaigns)
    group_rows = run_query(
        client,
        customer_id,
        f"""
        SELECT ad_group.id, ad_group.name, ad_group.status, campaign.id, campaign.name
        FROM ad_group
        WHERE campaign.id IN ({campaign_ids})
          AND ad_group.status != 'REMOVED'
        """,
    )
    criterion_rows = run_query(
        client,
        customer_id,
        f"""
        SELECT ad_group.id,
               ad_group_criterion.listing_group.case_value.product_item_id.value,
               ad_group_criterion.listing_group.type,
               ad_group_criterion.negative
        FROM ad_group_criterion
        WHERE campaign.id IN ({campaign_ids})
          AND ad_group_criterion.type = 'LISTING_GROUP'
          AND ad_group_criterion.status != 'REMOVED'
        """,
    )

    group_to_offer = {}
    for row in criterion_rows:
        if row.ad_group_criterion.negative:
            continue
        if row.ad_group_criterion.listing_group.type.name != "UNIT":
            continue
        offer_id = row.ad_group_criterion.listing_group.case_value.product_item_id.value
        if offer_id:
            group_to_offer.setdefault(str(row.ad_group.id), offer_id)

    enabled_offers_by_campaign = defaultdict(set)
    existing_groups_by_campaign_offer = defaultdict(list)
    existing_groups_by_campaign_name = defaultdict(list)
    all_groups = []
    for row in group_rows:
        group_id = str(row.ad_group.id)
        shared_catch_all = is_shared_catch_all_campaign(row.campaign.name)
        record = {
            "ad_group_id": group_id,
            "ad_group_name": row.ad_group.name,
            "ad_group_status": row.ad_group.status.name,
            "campaign_id": str(row.campaign.id),
            "campaign_name": row.campaign.name,
            "allowed": set() if shared_catch_all else parse_allowed_labels(row.campaign.name),
            "variant": CATCH_ALL_VARIANT if shared_catch_all else parse_campaign_variant(row.campaign.name),
            "shared_catch_all": shared_catch_all,
            "offer_id": group_to_offer.get(group_id, ""),
        }
        all_groups.append(record)
        existing_groups_by_campaign_name[(record["campaign_id"], normalize_text(record["ad_group_name"]))].append(record)
        if record["offer_id"]:
            existing_groups_by_campaign_offer[(record["campaign_id"], record["offer_id"])].append(record)
            if record["ad_group_status"] == "ENABLED":
                enabled_offers_by_campaign[record["campaign_id"]].add(record["offer_id"])

    title_counts_by_campaign = defaultdict(Counter)
    for campaign_id, campaign_products in products_by_campaign.items():
        title_counts_by_campaign[campaign_id].update(
            normalize_text(product.get("title") or "")
            for product in campaign_products
            if (product.get("title") or "").strip()
        )

    wrong_groups = []
    paused_product_groups = []
    groups_without_match = []
    active_groups_without_match = []
    rename_plan = []
    for group in all_groups:
        product = by_offer_id.get(group["offer_id"]) if group["offer_id"] else None
        match_via = "offer_id" if product else None
        if not product:
            candidates = by_title_norm.get(normalize_text(group["ad_group_name"])) or []
            if len(candidates) == 1:
                product = candidates[0]
                match_via = "title"
        if not product:
            groups_without_match.append(group)
            if group["ad_group_status"] == "ENABLED":
                active_groups_without_match.append(group)
            continue
        if product_has_paused_cl4(product):
            if group["ad_group_status"] == "ENABLED":
                paused_product_groups.append((group, product))
            continue
        label = (product.get("custom_label_1") or "").strip()
        if not label:
            if group["ad_group_status"] == "ENABLED":
                active_groups_without_match.append(group)
            continue
        target = target_by_offer.get((product.get("offer_id") or "").strip())
        if target is None:
            # Produkt istnieje w adsPRO, ale nie ma dla niego pasujacej kampanii (CL1/CL4).
            if group["ad_group_status"] == "ENABLED":
                active_groups_without_match.append(group)
            continue
        if group["campaign_id"] != target["id"]:
            # Zla kampania albo zly wariant (np. CL4 puste, a grupa w kampanii | catch_all).
            wrong_groups.append((group, product))
            continue
        title_key = normalize_text(product.get("title") or "")
        duplicate_title = False
        if target and title_key:
            duplicate_title = title_counts_by_campaign[target["id"]][title_key] > 1
        adspro_title = ad_group_name_for_product(product, duplicate_title)
        if (
            group["ad_group_status"] == "ENABLED"
            and match_via == "offer_id"
            and adspro_title
            and group["ad_group_name"] != adspro_title
        ):
            rename_plan.append(
                {
                    "ad_group_id": group["ad_group_id"],
                    "campaign_id": group["campaign_id"],
                    "campaign_name": group["campaign_name"],
                    "old_name": group["ad_group_name"],
                    "new_name": adspro_title,
                }
            )

    create_plan = []
    enable_by_id = {}
    pause_by_id = {}

    def plan_enable_or_create(campaign: dict, product: dict, fallback_name: str, reason: str) -> None:
        offer_id = (product.get("offer_id") or "").strip()
        title_key = normalize_text(product.get("title") or "")
        duplicate_title = title_key and title_counts_by_campaign[campaign["id"]][title_key] > 1
        title = ad_group_name_for_product(product, bool(duplicate_title)) or fallback_name
        if not offer_id or not title:
            return
        if offer_id in enabled_offers_by_campaign[campaign["id"]]:
            return

        # Najpierw szukamy grupy reklam po produkcie (offer_id), a dopiero
        # potem po nazwie. Dzieki temu istniejaca grupa z tym produktem, ale
        # pod inna nazwa, zostanie wlaczona i przemianowana, a nie zduplikowana.
        # Przy zduplikowanych tytulach nie dopasowujemy po samej nazwie, bo jedna
        # grupa reklam moze wtedy blednie pokryc kilka roznych offer_id.
        matched_via = None
        existing_candidates = existing_groups_by_campaign_offer.get((campaign["id"], offer_id), [])
        if existing_candidates:
            matched_via = "offer_id"
        elif not duplicate_title:
            existing_candidates = existing_groups_by_campaign_name.get((campaign["id"], normalize_text(title)), [])
            if existing_candidates:
                matched_via = "title"

        paused_candidate = next((group for group in existing_candidates if group["ad_group_status"] == "PAUSED"), None)
        if paused_candidate:
            enable_by_id[paused_candidate["ad_group_id"]] = {
                "ad_group_id": paused_candidate["ad_group_id"],
                "ad_group_name": paused_candidate["ad_group_name"],
                "campaign_id": paused_candidate["campaign_id"],
                "campaign_name": paused_candidate["campaign_name"],
                "product_id": offer_id,
                "reason": reason,
            }
            enabled_offers_by_campaign[campaign["id"]].add(offer_id)
            if matched_via == "offer_id" and paused_candidate["ad_group_name"] != title:
                rename_plan.append(
                    {
                        "ad_group_id": paused_candidate["ad_group_id"],
                        "campaign_id": paused_candidate["campaign_id"],
                        "campaign_name": paused_candidate["campaign_name"],
                        "old_name": paused_candidate["ad_group_name"],
                        "new_name": title,
                    }
                )
            return

        existing_active = next(
            (group for group in existing_candidates if group["ad_group_status"] == "ENABLED"),
            None,
        )
        if existing_active:
            enabled_offers_by_campaign[campaign["id"]].add(offer_id)
            return

        create_plan.append(
            {
                "campaign_id": campaign["id"],
                "campaign_name": campaign["name"],
                "ad_group_name": title,
                "product_id": offer_id,
                "reason": reason,
            }
        )
        enabled_offers_by_campaign[campaign["id"]].add(offer_id)

    wrong_variant_count = 0
    wrong_segment_count = 0
    for group, product in paused_product_groups:
        pause_by_id[group["ad_group_id"]] = {
            "ad_group_id": group["ad_group_id"],
            "ad_group_name": group["ad_group_name"],
            "campaign_id": group["campaign_id"],
            "campaign_name": group["campaign_name"],
            "reason": "produkt ma CL4=paused w adsPRO",
        }

    for group, product in wrong_groups:
        offer_id = (product.get("offer_id") or "").strip()
        target = target_by_offer.get(offer_id)
        label = (product.get("custom_label_1") or "").strip()
        if target and label in group["allowed"] and group["variant"] != target["variant"]:
            wrong_variant_count += 1
        else:
            wrong_segment_count += 1
        if target and offer_id:
            plan_enable_or_create(target, product, group["ad_group_name"], "produkt jest w zlej kampanii")
        if group["ad_group_status"] == "ENABLED":
            pause_by_id[group["ad_group_id"]] = {
                "ad_group_id": group["ad_group_id"],
                "ad_group_name": group["ad_group_name"],
                "campaign_id": group["campaign_id"],
                "campaign_name": group["campaign_name"],
                "reason": "produkt jest w zlej kampanii",
            }

    for campaign in campaigns:
        for product in products_by_campaign.get(campaign["id"], []):
            offer_id = (product.get("offer_id") or "").strip()
            title = (product.get("title") or "").strip()
            if not offer_id or not title:
                continue
            if offer_id in enabled_offers_by_campaign[campaign["id"]]:
                continue
            plan_enable_or_create(campaign, product, title, "brakuje aktywnej grupy reklam")

    for group in active_groups_without_match:
        pause_by_id[group["ad_group_id"]] = {
            "ad_group_id": group["ad_group_id"],
            "ad_group_name": group["ad_group_name"],
            "campaign_id": group["campaign_id"],
            "campaign_name": group["campaign_name"],
            "reason": "brak dopasowania w adsPRO",
        }

    grouped = defaultdict(list)
    for group in all_groups:
        if group["ad_group_status"] != "ENABLED" or group["ad_group_id"] in pause_by_id or not group["offer_id"]:
            continue
        grouped[(group["campaign_id"], group["offer_id"])].append(group)
    for group_list in grouped.values():
        if len(group_list) <= 1:
            continue
        for group in sorted(group_list, key=lambda item: int(item["ad_group_id"]))[:-1]:
            pause_by_id[group["ad_group_id"]] = {
                "ad_group_id": group["ad_group_id"],
                "ad_group_name": group["ad_group_name"],
                "campaign_id": group["campaign_id"],
                "campaign_name": group["campaign_name"],
                "reason": "duplikat produktu w kampanii",
            }

    pause_plan = [pause_by_id[key] for key in sorted(pause_by_id, key=int)]
    enable_plan = [enable_by_id[key] for key in sorted(enable_by_id, key=int)]
    pause_ids = set(pause_by_id)
    rename_plan = [row for row in rename_plan if row["ad_group_id"] not in pause_ids]

    # Google Ads wymaga unikalnych nazw grup reklam w obrebie kampanii.
    # Odrzucamy zmiany nazwy, ktore koliduja z inna grupa w tej samej kampanii
    # (np. kilka produktow adsPRO ma identyczny tytul).
    rename_by_id = {row["ad_group_id"]: row["new_name"] for row in rename_plan}
    final_names = defaultdict(Counter)
    for group in all_groups:
        name = rename_by_id.get(group["ad_group_id"], group["ad_group_name"])
        final_names[group["campaign_id"]][name] += 1
    safe_rename_plan = []
    rename_conflicts = 0
    for row in rename_plan:
        if final_names[row["campaign_id"]][row["new_name"]] > 1:
            rename_conflicts += 1
            continue
        safe_rename_plan.append(row)
    rename_plan = safe_rename_plan

    warnings = []
    if groups_without_match:
        warnings.append(f"Grupy reklam bez dopasowania w adsPRO: {len(groups_without_match)}.")
    warnings.extend(sorted(set(variant_warnings)))
    if shared_catch_all_offers:
        warnings.append(
            f"Produkty z CL4=catch_all przypisane do wspolnej kampanii [PLA_CL1] catch_all: "
            f"{len(shared_catch_all_offers)}."
        )
    if base_fallback_offers:
        warnings.append(
            f"Produkty z CL4, ale bez kampanii-wariantu, przypisane do kampanii bazowej: "
            f"{len(base_fallback_offers)}."
        )
    if paused_offers:
        warnings.append(
            f"Produkty z CL4=paused pominiete przy tworzeniu i wlaczaniu grup reklam: "
            f"{len(paused_offers)}."
        )
    if paused_product_groups:
        warnings.append(
            f"Aktywne grupy reklam produktow z CL4=paused do wstrzymania: "
            f"{len(paused_product_groups)}."
        )
    if orphan_offers:
        warnings.append(
            f"Produkty bez pasujacej kampanii PLA_CL1 (CL1/CL4): {len(orphan_offers)}."
        )
    if wrong_variant_count:
        warnings.append(
            f"Grupy reklam w zlym wariancie kampanii (CL4 nie pasuje): {wrong_variant_count}."
        )
    if wrong_segment_count:
        warnings.append(
            f"Grupy reklam w zlej kampanii (CL1 nie pasuje): {wrong_segment_count}."
        )
    if rename_conflicts:
        warnings.append(
            f"Zmiany nazwy pominiete z powodu kolizji nazw w kampanii "
            f"(zduplikowane tytuly w adsPRO): {rename_conflicts}."
        )

    return SyncPlan(
        campaigns=campaigns,
        groups_total=len(all_groups),
        groups_with_product_id=sum(1 for g in all_groups if g["offer_id"]),
        create_plan=create_plan,
        enable_plan=enable_plan,
        pause_plan=pause_plan,
        rename_plan=rename_plan,
        warnings=warnings,
        unmatched_groups=groups_without_match,
    )


def create_ad_group_with_listing(client, customer_id: str, campaign_id: str, product_id: str, ad_group_name: str):
    service = client.get_service("GoogleAdsService")
    ad_group_service = client.get_service("AdGroupService")
    campaign_resource = ad_group_service.campaign_path(customer_id, campaign_id)

    ad_group_temp = f"customers/{customer_id}/adGroups/-1"
    root_temp = f"customers/{customer_id}/adGroupCriteria/-1~-2"
    operations = []

    group_op = client.get_type("MutateOperation")
    group = group_op.ad_group_operation.create
    group.resource_name = ad_group_temp
    group.name = ad_group_name
    group.campaign = campaign_resource
    group.status = client.enums.AdGroupStatusEnum.ENABLED
    group.type_ = client.enums.AdGroupTypeEnum.SHOPPING_PRODUCT_ADS
    operations.append(group_op)

    root_op = client.get_type("MutateOperation")
    root = root_op.ad_group_criterion_operation.create
    root.resource_name = root_temp
    root.ad_group = ad_group_temp
    root.status = client.enums.AdGroupCriterionStatusEnum.ENABLED
    root.listing_group.type_ = client.enums.ListingGroupTypeEnum.SUBDIVISION
    operations.append(root_op)

    product_op = client.get_type("MutateOperation")
    product = product_op.ad_group_criterion_operation.create
    product.ad_group = ad_group_temp
    product.status = client.enums.AdGroupCriterionStatusEnum.ENABLED
    product.listing_group.type_ = client.enums.ListingGroupTypeEnum.UNIT
    product.listing_group.parent_ad_group_criterion = root_temp
    product.listing_group.case_value.product_item_id.value = product_id
    product.cpc_bid_micros = 1_000_000
    operations.append(product_op)

    other_op = client.get_type("MutateOperation")
    other = other_op.ad_group_criterion_operation.create
    other.ad_group = ad_group_temp
    other.negative = True
    other.status = client.enums.AdGroupCriterionStatusEnum.ENABLED
    other.listing_group.type_ = client.enums.ListingGroupTypeEnum.UNIT
    other.listing_group.parent_ad_group_criterion = root_temp
    client.copy_from(other.listing_group.case_value.product_item_id, client.get_type("ProductItemIdInfo"))
    operations.append(other_op)

    ad_op = client.get_type("MutateOperation")
    ad = ad_op.ad_group_ad_operation.create
    ad.ad_group = ad_group_temp
    ad.status = client.enums.AdGroupAdStatusEnum.ENABLED
    ad.ad.shopping_product_ad._pb.SetInParent()
    operations.append(ad_op)

    service.mutate(customer_id=customer_id, mutate_operations=operations)


def pause_ad_groups(client, customer_id: str, ad_group_ids: list[str]) -> int:
    service = client.get_service("AdGroupService")
    changed = 0
    for index in range(0, len(ad_group_ids), 500):
        operations = []
        for ad_group_id in ad_group_ids[index:index + 500]:
            op = client.get_type("AdGroupOperation")
            group = op.update
            group.resource_name = service.ad_group_path(customer_id, ad_group_id)
            group.status = client.enums.AdGroupStatusEnum.PAUSED
            op.update_mask = field_mask_pb2.FieldMask(paths=["status"])
            operations.append(op)
        if operations:
            request = client.get_type("MutateAdGroupsRequest")
            request.customer_id = customer_id
            request.operations = operations
            request.partial_failure = True
            response = service.mutate_ad_groups(request=request)
            changed += sum(1 for result in response.results if result.resource_name)
    return changed


def enable_ad_groups(client, customer_id: str, ad_group_ids: list[str]) -> int:
    if not ad_group_ids:
        return 0
    service = client.get_service("AdGroupService")
    changed = 0
    for index in range(0, len(ad_group_ids), 500):
        operations = []
        for ad_group_id in ad_group_ids[index:index + 500]:
            op = client.get_type("AdGroupOperation")
            group = op.update
            group.resource_name = service.ad_group_path(customer_id, ad_group_id)
            group.status = client.enums.AdGroupStatusEnum.ENABLED
            op.update_mask = field_mask_pb2.FieldMask(paths=["status"])
            operations.append(op)
        if operations:
            request = client.get_type("MutateAdGroupsRequest")
            request.customer_id = customer_id
            request.operations = operations
            request.partial_failure = True
            response = service.mutate_ad_groups(request=request)
            changed += sum(1 for result in response.results if result.resource_name)
    return changed


def rename_ad_groups(client, customer_id: str, renames: list[dict]) -> int:
    service = client.get_service("AdGroupService")
    changed = 0
    for index in range(0, len(renames), 500):
        operations = []
        for row in renames[index:index + 500]:
            op = client.get_type("AdGroupOperation")
            group = op.update
            group.resource_name = service.ad_group_path(customer_id, row["ad_group_id"])
            group.name = row["new_name"]
            op.update_mask = field_mask_pb2.FieldMask(paths=["name"])
            operations.append(op)
        if operations:
            request = client.get_type("MutateAdGroupsRequest")
            request.customer_id = customer_id
            request.operations = operations
            request.partial_failure = True
            response = service.mutate_ad_groups(request=request)
            changed += sum(1 for result in response.results if result.resource_name)
    return changed


def print_plan(plan: SyncPlan) -> None:
    print("\nPlan synchronizacji PLA_CL1")
    print(f"Kampanie PLA_CL1: {len(plan.campaigns)}")
    print(f"Grupy reklam obecnie: {plan.groups_total}")
    print(f"Grupy reklam z identyfikatorem produktu: {plan.groups_with_product_id}")
    print(f"Do utworzenia: {len(plan.create_plan)}")
    print(f"Do włączenia: {len(plan.enable_plan)}")
    print(f"Do wstrzymania: {len(plan.pause_plan)}")
    print(f"Do zmiany nazwy: {len(plan.rename_plan)}")
    for warning in plan.warnings:
        print(f"Uwaga: {warning}")

    summary = campaign_action_summary(plan)
    if summary:
        print("\nPodsumowanie po kampaniach:")
        for row in summary:
            print(
                f"  {row['campaign_name']} | "
                f"utwórz={row['create']} | włącz={row['enable']} | "
                f"wstrzymaj={row['pause']} | zmień nazwę={row['rename']}"
            )

    for row in plan.create_plan[:20]:
        print(f"  Utworz: {row['campaign_name']} | {row['ad_group_name']} | {row['product_id']}")
    if len(plan.create_plan) > 20:
        print(f"  ... oraz {len(plan.create_plan) - 20} kolejnych grup reklam do utworzenia")
    for row in plan.enable_plan[:20]:
        print(f"  Włącz: {row['campaign_name']} | {row['ad_group_name']} | {row['product_id']}")
    if len(plan.enable_plan) > 20:
        print(f"  ... oraz {len(plan.enable_plan) - 20} kolejnych grup reklam do włączenia")
    for row in plan.pause_plan[:20]:
        print(f"  Wstrzymaj: {row['campaign_name']} | {row['ad_group_name']} | {row['reason']}")
    if len(plan.pause_plan) > 20:
        print(f"  ... oraz {len(plan.pause_plan) - 20} kolejnych grup reklam do wstrzymania")
    for row in plan.rename_plan[:20]:
        print(f"  Zmien nazwe: {row['ad_group_id']} | {row['old_name'][:50]} -> {row['new_name'][:50]}")
    if len(plan.rename_plan) > 20:
        print(f"  ... oraz {len(plan.rename_plan) - 20} kolejnych nazw do zmiany")


def print_next_navigation(domain: str) -> None:
    print("\nCo dalej:")
    print(f"1. Lista zadan klienta {domain}")
    print("2. Lista klientow")
    print("3. Zakoncz")
    print("\nKomendy:")
    print(f"1 -> python gads.py analiza-klienta --client {domain}")
    print("2 -> python gads.py analiza-klienta")


def apply_sync_plan(client_config: ClientConfig, plan: SyncPlan, show_navigation: bool = True) -> None:
    google_client = get_google_ads_client(use_proto_plus=True)
    customer_id = client_config.safe_customer_id

    created = 0
    create_errors = 0
    for row in plan.create_plan:
        try:
            create_ad_group_with_listing(
                google_client,
                customer_id,
                row["campaign_id"],
                row["product_id"],
                row["ad_group_name"],
            )
            created += 1
        except Exception as exc:
            create_errors += 1
            print(f"Blad tworzenia grupy reklam {row['ad_group_name']}: {exc}")

    pause_ids = [row["ad_group_id"] for row in plan.pause_plan]
    enable_ids = [row["ad_group_id"] for row in plan.enable_plan]
    enabled = enable_ad_groups(google_client, customer_id, enable_ids) if enable_ids else 0
    paused = pause_ad_groups(google_client, customer_id, pause_ids) if pause_ids else 0
    renamed = rename_ad_groups(google_client, customer_id, plan.rename_plan) if plan.rename_plan else 0

    print("\nWynik wdrozenia zmian")
    print(f"Utworzono grup reklam: {created}")
    print(f"Włączono grup reklam: {enabled}")
    print(f"Bledy tworzenia: {create_errors}")
    print(f"Wstrzymano grup reklam: {paused}")
    print(f"Zmieniono nazwy grup reklam: {renamed}")

    rows = []
    rows.extend(
        {
            "klient": client_config.domain,
            "kampania": row["campaign_name"],
            "czynnosc": "włączono grupę reklam",
            "grupa reklam": row["ad_group_name"],
            "produkt": row["product_id"],
        }
        for row in plan.enable_plan
    )
    rows.extend(
        {
            "klient": client_config.domain,
            "kampania": row["campaign_name"],
            "czynnosc": "utworzono grupe reklam",
            "grupa reklam": row["ad_group_name"],
            "produkt": row["product_id"],
        }
        for row in plan.create_plan
    )
    rows.extend(
        {
            "klient": client_config.domain,
            "kampania": row["campaign_name"],
            "czynnosc": "wstrzymano grupe reklam",
            "grupa reklam": row["ad_group_name"],
            "produkt": row["reason"],
        }
        for row in plan.pause_plan
    )
    rows.extend(
        {
            "klient": client_config.domain,
            "kampania": row["campaign_name"],
            "czynnosc": "zmieniono nazwe grupy reklam",
            "grupa reklam": row["old_name"],
            "produkt": row["new_name"],
        }
        for row in plan.rename_plan
    )
    changes_path = append_change_markdown(client_config.domain, "Synchronizacja kampanii PLA_CL1", rows)
    history_path = append_history(
        client_config.domain,
        {
            "task": "Synchronizacja kampanii PLA_CL1",
            "status": "wdrozono zmiany",
            "campaign": ", ".join(c["name"] for c in plan.campaigns[:10]),
            "summary": {
                "created": created,
                "enabled": enabled,
                "create_errors": create_errors,
                "paused": paused,
                "renamed": renamed,
            },
        },
    )
    print(f"Historia JSONL: {history_path}")
    print(f"Historia Markdown: {changes_path}")
    if show_navigation:
        print_next_navigation(client_config.domain)


def run_sync_pla_cl1(
    client_config: ClientConfig,
    global_rules: dict,
    plan_only: bool = False,
    apply_plan_path: str | None = None,
    confirm_apply: str | None = None,
    show_navigation: bool = True,
) -> None:
    if apply_plan_path:
        if confirm_apply != "TAK":
            print("Do wdrozenia planu wymagane jest --confirm-apply TAK.")
            if show_navigation:
                print_next_navigation(client_config.domain)
            return
        plan_data = json.loads(Path(apply_plan_path).read_text(encoding="utf-8"))
        if plan_data.get("client") != client_config.domain:
            print(
                f"Plan jest dla klienta {plan_data.get('client')}, "
                f"a wybrano {client_config.domain}."
            )
            if show_navigation:
                print_next_navigation(client_config.domain)
            return
        plan = SyncPlan.from_dict(plan_data)
        print_plan(plan)
        apply_sync_plan(client_config, plan, show_navigation=show_navigation)
        return

    started = now_local()
    print(f"\nKlient: {client_config.domain}")
    print("Pobieram kampanie PLA_CL1 i produkty z adsPRO...")

    google_client = get_google_ads_client(use_proto_plus=True)
    customer_id = client_config.safe_customer_id

    campaign_rows = run_query(
        google_client,
        customer_id,
        """
        SELECT campaign.id, campaign.name, campaign.status
        FROM campaign
        WHERE campaign.name LIKE '%PLA_CL1%'
          AND campaign.status = 'ENABLED'
        """,
    )
    segments = sorted(
        {
            label
            for row in campaign_rows
            if not is_shared_catch_all_campaign(row.campaign.name)
            for label in parse_allowed_labels(row.campaign.name)
        }
    )
    if not segments:
        print("Nie znaleziono segmentow CL1 w kampaniach [PLA_CL1].")
        append_history(
            client_config.domain,
            {
                "task": "Synchronizacja kampanii PLA_CL1",
                "status": "brak kampanii",
                "campaign": "",
            },
        )
        if show_navigation:
            print_next_navigation(client_config.domain)
        return

    print("Segmenty CL1: " + ", ".join(segments))
    products = fetch_adspro_products(client_config, segments)
    products_path = save_products_csv(client_config.domain, products)
    print(f"Pobrano produkty z adsPRO: {len(products)}")
    print(f"Zapisano dane: {products_path}")

    plan = build_plan(google_client, customer_id, products)
    print_plan(plan)
    json_path, md_path = save_plan_files(client_config.domain, plan, len(products))
    print(f"\nPlan JSON: {json_path}")
    print(f"Plan Markdown: {md_path}")

    append_history(
        client_config.domain,
        {
            "task": "Synchronizacja kampanii PLA_CL1",
            "status": "plan przygotowany",
            "campaign": ", ".join(c["name"] for c in plan.campaigns[:10]),
            "created_at": started.isoformat(timespec="seconds"),
            "summary": {
                "campaigns": len(plan.campaigns),
                "products": len(products),
                "create": len(plan.create_plan),
                "enable": len(plan.enable_plan),
                "pause": len(plan.pause_plan),
                "rename": len(plan.rename_plan),
            },
        },
    )

    if plan_only:
        print("\nTryb plan-only: zmiany nie zostaly wdrozone.")
        if show_navigation:
            print_next_navigation(client_config.domain)
        return

    if not plan.create_plan and not plan.enable_plan and not plan.pause_plan and not plan.rename_plan:
        print("\nBrak zmian do wdrozenia.")
        append_change_markdown(client_config.domain, "Synchronizacja kampanii PLA_CL1", [])
        if show_navigation:
            print_next_navigation(client_config.domain)
        return

    answer = input("\nWpisz TAK, aby wdrozyc powyzsze zmiany: ").strip()
    if answer != "TAK":
        print("Przerwano. Zmiany nie zostaly wdrozone.")
        append_history(
            client_config.domain,
            {
                "task": "Synchronizacja kampanii PLA_CL1",
                "status": "odrzucono wdrozenie",
                "campaign": ", ".join(c["name"] for c in plan.campaigns[:10]),
            },
        )
        if show_navigation:
            print_next_navigation(client_config.domain)
        return

    apply_sync_plan(client_config, plan, show_navigation=show_navigation)
