from __future__ import annotations

import json
from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any

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
from ..knowledge.store import rules_for_task
from ..table import print_table


TASK_ID = "check_bidding_strategies"
TASK_NAME = "Sprawdzenie strategii stawek"


SCOPE = [
    {
        "area": "Typ strategii",
        "check": "Pokaz typ strategii ustalania stawek dla aktywnych kampanii.",
    },
    {
        "area": "Cele strategii",
        "check": "Pokaz aktualny Docelowy ROAS albo Docelowy CPA, jezeli kampania go uzywa.",
    },
    {
        "area": "Wolumen konwersji",
        "check": "Sprawdz liczbe konwersji z ostatnich 30 dni jako kontekst dla automatycznych strategii.",
    },
    {
        "area": "Stabilnosc decyzji",
        "check": "Oznacz kampanie, gdzie malo danych zwieksza ryzyko pochopnej zmiany strategii albo celu.",
    },
    {
        "area": "Ocena celu",
        "check": "Porownaj rzeczywisty ROAS/CPA z aktualnym celem i oznacz cele zbyt niskie albo zbyt wysokie.",
    },
    {
        "area": "Limit zmiany ROAS",
        "check": "Nie proponuj jednorazowego podniesienia Docelowego ROAS o wiecej niz 5 punktow procentowych.",
    },
    {
        "area": "Kontekst budzetu",
        "check": "Uwzglednij wykorzystanie budzetu i utrate wyswietlania przez budzet przed rekomendacja zmiany strategii.",
    },
    {
        "area": "Zmiany po budzecie",
        "check": "Jesli budzet byl niedawno zmieniany, rekomenduj odczekanie przed zmiana strategii albo celu.",
    },
    {
        "area": "Dopasowanie strategii",
        "check": "Sprawdz, czy strategia pasuje do typu kampanii i dostepnego wolumenu danych.",
    },
    {
        "area": "Rekomendacja",
        "check": "Pokaz konkretna rekomendacje decyzyjna bez automatycznego wdrazania zmian strategii.",
    },
]


OUT_OF_SCOPE = [
    "budzety i pacing budzetu",
    "podstawowe ustawienia kampanii, np. lokalizacje i sieci",
    "zapytania uzytkownikow oraz wykluczenia",
    "reklamy, zasoby i kreacje",
    "automatyczne wdrazanie zmian strategii stawek",
]


LOW_CONVERSION_THRESHOLD = 15
STABLE_CONVERSION_THRESHOLD = 30
TARGET_ROAS_TOO_LOW_RATIO = 1.5
TARGET_ROAS_TOO_HIGH_RATIO = 0.75
BUDGET_LOST_THRESHOLD = 0.15
HIGH_BUDGET_LOST_THRESHOLD = 0.3
RECENT_BUDGET_CHANGE_DAYS = 7
MAX_TARGET_ROAS_INCREASE_POINTS = 0.05


@dataclass
class BiddingStrategyPlan:
    currency_code: str
    campaigns: list[dict]
    scope: list[dict]
    out_of_scope: list[str]
    knowledge_rules: list[dict]
    warnings: list[str]
    target_changes: list[dict] | None = None

    def to_dict(self) -> dict:
        return {
            "task": TASK_ID,
            "task_name": TASK_NAME,
            "currency_code": self.currency_code,
            "campaigns": self.campaigns,
            "scope": self.scope,
            "out_of_scope": self.out_of_scope,
            "knowledge_rules": self.knowledge_rules,
            "warnings": self.warnings,
            "target_changes": self.target_changes or [],
            "changes": self.target_changes or [],
        }

    @classmethod
    def from_dict(cls, data: dict) -> "BiddingStrategyPlan":
        return cls(
            currency_code=data.get("currency_code", ""),
            campaigns=data.get("campaigns", []),
            scope=data.get("scope", []),
            out_of_scope=data.get("out_of_scope", []),
            knowledge_rules=data.get("knowledge_rules", []),
            warnings=data.get("warnings", []),
            target_changes=data.get("target_changes", data.get("changes", [])),
        )


def enum_name(value: Any) -> str:
    name = getattr(value, "name", None)
    if name:
        return name
    return str(value)


def micros_to_amount(value: int | float) -> float:
    return round(float(value or 0) / 1_000_000, 2)


def format_money(value: int | float, currency_code: str) -> str:
    suffix = f" {currency_code}" if currency_code else ""
    return f"{micros_to_amount(value):.2f}{suffix}"


def format_decimal(value: int | float) -> str:
    return f"{float(value or 0):.2f}"


def percent_label(value: int | float | None) -> str:
    if value is None:
        return "-"
    return f"{float(value) * 100:.2f}%"


def safe_int(value: Any) -> int:
    try:
        return int(value or 0)
    except (TypeError, ValueError):
        return 0


def safe_float(value: Any) -> float:
    try:
        return float(value or 0)
    except (TypeError, ValueError):
        return 0.0


def target_roas_label(value: float) -> str:
    if value <= 0:
        return ""
    return f"Docelowy ROAS {value * 100:.0f}%"


def target_cpa_label(value_micros: int, currency_code: str) -> str:
    if value_micros <= 0:
        return ""
    return f"Docelowy CPA {format_money(value_micros, currency_code)}"


def strategy_target_label(row: dict, currency_code: str) -> str:
    labels = [
        target_roas_label(row.get("target_roas", 0)),
        target_roas_label(row.get("maximize_conversion_value_target_roas", 0)),
        target_cpa_label(row.get("target_cpa_micros", 0), currency_code),
        target_cpa_label(row.get("maximize_conversions_target_cpa_micros", 0), currency_code),
    ]
    return ", ".join(label for label in labels if label) or "brak jawnego celu"


def risk_label(strategy_type: str, conversions_30d: float) -> str:
    strategy = strategy_type.upper()
    automated = {
        "MAXIMIZE_CONVERSIONS",
        "MAXIMIZE_CONVERSION_VALUE",
        "TARGET_CPA",
        "TARGET_ROAS",
    }
    if conversions_30d <= 0:
        return "brak konwersji w 30 dni"
    if strategy in automated and conversions_30d < LOW_CONVERSION_THRESHOLD:
        return "malo konwersji dla automatyzacji"
    return "dane do oceny"


def actual_roas(cost_micros: int, conversion_value: float) -> float:
    cost = micros_to_amount(cost_micros)
    if cost <= 0:
        return 0.0
    return round(float(conversion_value or 0) / cost, 2)


def primary_target_roas(row: dict) -> float:
    return safe_float(row.get("target_roas")) or safe_float(row.get("maximize_conversion_value_target_roas"))


def primary_target_cpa_micros(row: dict) -> int:
    return safe_int(row.get("target_cpa_micros")) or safe_int(row.get("maximize_conversions_target_cpa_micros"))


def target_assessment(row: dict) -> str:
    target_roas = primary_target_roas(row)
    roas = safe_float(row.get("actual_roas"))
    conversions = safe_float(row.get("conversions_30d"))
    if target_roas > 0 and roas > 0:
        if conversions < LOW_CONVERSION_THRESHOLD:
            return "za malo danych do oceny celu ROAS"
        if roas >= target_roas * TARGET_ROAS_TOO_LOW_RATIO:
            return "Docelowy ROAS prawdopodobnie za niski"
        if roas <= target_roas * TARGET_ROAS_TOO_HIGH_RATIO:
            return "Docelowy ROAS prawdopodobnie za wysoki"
        return "Docelowy ROAS zgodny z wynikiem"
    if primary_target_cpa_micros(row) > 0:
        if conversions < LOW_CONVERSION_THRESHOLD:
            return "za malo danych do oceny celu CPA"
        return "Docelowy CPA do oceny po koszcie na konwersje"
    return "brak jawnego celu do oceny"


def stability_label(row: dict) -> str:
    conversions = safe_float(row.get("conversions_30d"))
    conversion_value = safe_float(row.get("conversion_value_30d"))
    cost = safe_int(row.get("cost_30d_micros"))
    if conversions <= 0:
        return "niestabilne: brak konwersji"
    if conversions < LOW_CONVERSION_THRESHOLD:
        return "niestabilne: malo konwersji"
    if conversions < STABLE_CONVERSION_THRESHOLD:
        return "umiarkowane: dane do ostroznej decyzji"
    if conversion_value <= 0 or cost <= 0:
        return "niestabilne: brak wartosci albo kosztu"
    return "stabilne"


def budget_context(row: dict, recent_budget_changes: set[str]) -> str:
    if row["campaign_id"] in recent_budget_changes or row["campaign_name"] in recent_budget_changes:
        return f"budzet zmieniony w ostatnich {RECENT_BUDGET_CHANGE_DAYS} dniach"
    lost_budget = safe_float(row.get("search_budget_lost_impression_share"))
    usage = safe_float(row.get("budget_usage_percent"))
    if lost_budget >= HIGH_BUDGET_LOST_THRESHOLD:
        return "mocne ograniczenie budzetem"
    if lost_budget >= BUDGET_LOST_THRESHOLD:
        return "ograniczenie budzetem do oceny"
    if usage >= 95:
        return "budzet blisko limitu"
    if usage and usage < 30:
        return "niskie wykorzystanie budzetu"
    return "brak silnego sygnalu budzetowego"


def strategy_fit(row: dict) -> str:
    strategy = row.get("bidding_strategy_type", "").upper()
    channel = row.get("channel_type", "").upper()
    conversions = safe_float(row.get("conversions_30d"))
    if strategy == "TARGET_IMPRESSION_SHARE" and channel != "SEARCH":
        return "do sprawdzenia: udzial w wyswietleniach poza Search"
    if strategy == "TARGET_IMPRESSION_SHARE":
        return "pasuje do kampanii brand/search, nie optymalizuje bezposrednio wartosci konwersji"
    if strategy in {"TARGET_ROAS", "MAXIMIZE_CONVERSION_VALUE"}:
        if conversions < LOW_CONVERSION_THRESHOLD:
            return "ryzykowne: za malo konwersji dla strategii wartosci"
        return "pasuje do kampanii e-commerce z wartoscia konwersji"
    if strategy in {"TARGET_CPA", "MAXIMIZE_CONVERSIONS"}:
        if conversions < LOW_CONVERSION_THRESHOLD:
            return "ryzykowne: za malo konwersji dla automatyzacji"
        return "pasuje do celu pozyskiwania konwersji"
    return "do oceny recznej"


def bidding_recommendation(row: dict) -> dict:
    if "budzet zmieniony" in row.get("budget_context", ""):
        return {
            "level": "czekaj",
            "action": "odczekaj przed zmiana strategii",
            "reason": "budzet byl niedawno zmieniony; najpierw zbierz nowe dane po zmianie",
        }
    if row.get("budget_context") in {"mocne ograniczenie budzetem", "ograniczenie budzetem do oceny"}:
        return {
            "level": "czekaj",
            "action": "najpierw rozwiaz ograniczenie budzetem",
            "reason": "zmiana celu strategii przy ograniczeniu budzetem moze zaciemnic efekt decyzji",
        }
    if row.get("stability_label", "").startswith("niestabilne"):
        return {
            "level": "ostroznie",
            "action": "nie zmieniaj strategii",
            "reason": row["stability_label"],
        }
    assessment = row.get("target_assessment", "")
    if "za niski" in assessment:
        return {
            "level": "do decyzji",
            "action": "rozwaz stopniowe podniesienie Docelowego ROAS",
            "reason": "rzeczywisty ROAS jest wyraznie wyzszy niz cel i dane sa wystarczajace",
        }
    if "za wysoki" in assessment:
        return {
            "level": "do decyzji",
            "action": "rozwaz obnizenie Docelowego ROAS albo analize rentownosci",
            "reason": "rzeczywisty ROAS jest wyraznie ponizej celu",
        }
    return {
        "level": "ok",
        "action": "bez zmiany strategii",
        "reason": "brak mocnego sygnalu do zmiany celu albo strategii",
    }


def recent_budget_change_campaigns(domain: str) -> set[str]:
    history_dir = client_dir(domain) / "history"
    changed: set[str] = set()
    if not history_dir.exists():
        return changed
    cutoff = now_local() - timedelta(days=RECENT_BUDGET_CHANGE_DAYS)
    for path in history_dir.glob("*.jsonl"):
        try:
            for line in path.read_text(encoding="utf-8-sig").splitlines():
                if not line.strip():
                    continue
                row = json.loads(line)
                timestamp = row.get("timestamp")
                if timestamp:
                    try:
                        if datetime.fromisoformat(timestamp) < cutoff:
                            continue
                    except ValueError:
                        pass
                summary = row.get("summary", {})
                if not summary.get("budget_changes"):
                    continue
                campaign_text = str(row.get("campaign", ""))
                for name in campaign_text.split(","):
                    name = name.strip()
                    if name:
                        changed.add(name)
        except Exception:
            continue
    return changed


def bidding_recommendations(campaigns: list[dict]) -> list[dict]:
    return [
        campaign
        for campaign in campaigns
        if campaign.get("bidding_recommendation", {}).get("level") not in {"ok", None}
    ]


def roas_target_field(row: dict) -> str:
    strategy = row.get("bidding_strategy_type", "").upper()
    if strategy == "TARGET_ROAS" and safe_float(row.get("target_roas")) > 0:
        return "target_roas.target_roas"
    if strategy == "MAXIMIZE_CONVERSION_VALUE" and safe_float(row.get("maximize_conversion_value_target_roas")) > 0:
        return "maximize_conversion_value.target_roas"
    return ""


def suggested_target_roas_change(row: dict) -> dict | None:
    field = roas_target_field(row)
    if not field:
        return None
    recommendation = row.get("bidding_recommendation", {})
    if recommendation.get("level") != "do decyzji":
        return None

    current = primary_target_roas(row)
    actual = safe_float(row.get("actual_roas"))
    if current <= 0 or actual <= 0:
        return None

    assessment = row.get("target_assessment", "")
    if "za niski" in assessment:
        target = min(current * 1.10, current + MAX_TARGET_ROAS_INCREASE_POINTS, actual * 0.80)
        direction = "podniesienie"
    elif "za wysoki" in assessment:
        target = max(current * 0.90, actual * 1.20)
        direction = "obnizenie"
    else:
        return None

    target = round(target, 4)
    if abs(target - current) < 0.0001:
        return None

    return {
        "campaign_id": row["campaign_id"],
        "campaign_name": row["campaign_name"],
        "strategy": row["bidding_strategy_type"],
        "field": field,
        "current_target_roas": round(current, 4),
        "target_roas": target,
        "current_label": f"{current * 100:.1f}%",
        "target_label": f"{target * 100:.1f}%",
        "actual_roas": actual,
        "direction": direction,
        "reason": recommendation.get("reason", ""),
    }


def target_change_candidates(campaigns: list[dict]) -> list[dict]:
    changes = []
    for campaign in campaigns:
        change = suggested_target_roas_change(campaign)
        if change:
            changes.append(change)
    return changes


def fetch_bidding_targets(google_client, customer_id: str, warnings: list[str]) -> dict[str, dict]:
    try:
        rows = run_query(
            google_client,
            customer_id,
            """
            SELECT
              campaign.id,
              campaign.target_cpa.target_cpa_micros,
              campaign.target_roas.target_roas,
              campaign.maximize_conversions.target_cpa_micros,
              campaign.maximize_conversion_value.target_roas
            FROM campaign
            WHERE campaign.status = 'ENABLED'
            """,
        )
    except Exception as exc:
        warnings.append(f"Nie udalo sie pobrac celow Docelowy ROAS/CPA: {exc}")
        return {}

    targets: dict[str, dict] = {}
    for row in rows:
        campaign = row.campaign
        targets[str(campaign.id)] = {
            "target_cpa_micros": safe_int(campaign.target_cpa.target_cpa_micros),
            "target_roas": safe_float(campaign.target_roas.target_roas),
            "maximize_conversions_target_cpa_micros": safe_int(
                campaign.maximize_conversions.target_cpa_micros
            ),
            "maximize_conversion_value_target_roas": safe_float(
                campaign.maximize_conversion_value.target_roas
            ),
        }
    return targets


def fetch_bidding_campaigns(client_config: ClientConfig) -> tuple[str, list[dict], list[str]]:
    warnings: list[str] = []
    google_client = get_google_ads_client(use_proto_plus=True)
    customer_id = client_config.safe_customer_id
    targets_by_campaign = fetch_bidding_targets(google_client, customer_id, warnings)
    rows = run_query(
        google_client,
        customer_id,
        """
        SELECT
          customer.currency_code,
          campaign.id,
          campaign.name,
          campaign.status,
          campaign.advertising_channel_type,
          campaign.bidding_strategy_type,
          campaign.bidding_strategy,
          campaign_budget.id,
          campaign_budget.name,
          campaign_budget.amount_micros,
          metrics.cost_micros,
          metrics.conversions,
          metrics.conversions_value,
          metrics.search_impression_share,
          metrics.search_budget_lost_impression_share,
          metrics.search_rank_lost_impression_share
        FROM campaign
        WHERE campaign.status = 'ENABLED'
          AND segments.date DURING LAST_30_DAYS
        """,
    )

    currency_code = ""
    campaigns = []
    for row in rows:
        currency_code = currency_code or str(row.customer.currency_code or "")
        campaign = row.campaign
        campaign_id = str(campaign.id)
        record = {
            "campaign_id": campaign_id,
            "campaign_name": campaign.name,
            "status": enum_name(campaign.status),
            "channel_type": enum_name(campaign.advertising_channel_type),
            "bidding_strategy_type": enum_name(campaign.bidding_strategy_type),
            "bidding_strategy_resource": str(campaign.bidding_strategy or ""),
            "budget_id": str(row.campaign_budget.id),
            "budget_name": row.campaign_budget.name,
            "daily_budget_micros": safe_int(row.campaign_budget.amount_micros),
            "cost_30d_micros": safe_int(row.metrics.cost_micros),
            "conversions_30d": safe_float(row.metrics.conversions),
            "conversion_value_30d": safe_float(row.metrics.conversions_value),
            "search_impression_share": safe_float(row.metrics.search_impression_share),
            "search_budget_lost_impression_share": safe_float(row.metrics.search_budget_lost_impression_share),
            "search_rank_lost_impression_share": safe_float(row.metrics.search_rank_lost_impression_share),
        }
        expected_30d_micros = record["daily_budget_micros"] * 30
        record["budget_usage_percent"] = (
            round((record["cost_30d_micros"] / expected_30d_micros) * 100, 1)
            if expected_30d_micros
            else 0.0
        )
        record["actual_roas"] = actual_roas(record["cost_30d_micros"], record["conversion_value_30d"])
        record.update(targets_by_campaign.get(campaign_id, {}))
        record["target_label"] = strategy_target_label(record, currency_code)
        record["risk_label"] = risk_label(record["bidding_strategy_type"], record["conversions_30d"])
        campaigns.append(record)
    return currency_code, campaigns, warnings


def build_bidding_strategy_plan(client_config: ClientConfig) -> BiddingStrategyPlan:
    warnings = []
    try:
        currency_code, campaigns, fetch_warnings = fetch_bidding_campaigns(client_config)
        warnings.extend(fetch_warnings)
    except Exception as exc:
        currency_code = ""
        campaigns = []
        warnings.append(f"Nie udalo sie pobrac strategii stawek z Google Ads API: {exc}")

    if not campaigns:
        warnings.append("Nie znaleziono aktywnych kampanii z danymi z ostatnich 30 dni albo nie udalo sie ich pobrac.")

    recent_budget_changes = recent_budget_change_campaigns(client_config.domain)
    for campaign in campaigns:
        campaign["target_assessment"] = target_assessment(campaign)
        campaign["stability_label"] = stability_label(campaign)
        campaign["budget_context"] = budget_context(campaign, recent_budget_changes)
        campaign["strategy_fit"] = strategy_fit(campaign)
        campaign["bidding_recommendation"] = bidding_recommendation(campaign)

    rules = rules_for_task(TASK_ID)
    knowledge_rules = [
        {
            "id": rule.id,
            "topic": rule.topic,
            "rule_type": rule.rule_type,
            "condition": rule.condition,
            "recommendation": rule.recommendation,
            "risk": rule.risk,
            "source": rule.source,
        }
        for rule in rules
    ]
    if not knowledge_rules:
        warnings.append(
            "Do tego zadania nie przypisano jeszcze regul z bazy wiedzy. "
            "Reguly dotyczace strategii stawek bedziemy dopisywac osobno po akceptacji uzytkownika."
        )

    recommendation_order = {"czekaj": 0, "do decyzji": 1, "ostroznie": 2, "ok": 9}
    campaigns.sort(
        key=lambda row: (
            recommendation_order.get(row.get("bidding_recommendation", {}).get("level"), 9),
            row["risk_label"],
            row["campaign_name"],
        )
    )
    target_changes = target_change_candidates(campaigns)
    return BiddingStrategyPlan(
        currency_code=currency_code,
        campaigns=campaigns,
        scope=SCOPE,
        out_of_scope=OUT_OF_SCOPE,
        knowledge_rules=knowledge_rules,
        warnings=warnings,
        target_changes=target_changes,
    )


def save_bidding_strategy_plan(domain: str, plan: BiddingStrategyPlan) -> 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')}_{TASK_ID}"
    json_path = base / f"{stem}.json"
    md_path = base / f"{stem}.md"
    payload = {
        "created_at": ts.isoformat(timespec="seconds"),
        "client": domain,
        **plan.to_dict(),
    }
    json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")

    lines = [
        "# Plan: Sprawdzenie strategii stawek",
        "",
        f"Klient: {domain}",
        f"Utworzono: {ts.isoformat(timespec='seconds')}",
        "",
        "## Podsumowanie",
        "",
        f"- Kampanie aktywne z danymi 30 dni: {len(plan.campaigns)}",
        f"- Reguly wiedzy przypisane do zadania: {len(plan.knowledge_rules)}",
        f"- Rekomendacje strategii do decyzji: {len(bidding_recommendations(plan.campaigns))}",
        f"- Zmiany celu do wdrozenia po akceptacji: {len(plan.target_changes or [])}",
        "",
    ]
    if plan.warnings:
        lines.extend(["## Uwagi", ""])
        lines.extend(f"- {item}" for item in plan.warnings)
        lines.append("")
    lines.extend(["## Zakres zadania", "", "| Obszar | Co sprawdzic |", "| --- | --- |"])
    for row in plan.scope:
        lines.append(f"| {row.get('area', '')} | {row.get('check', '')} |")
    lines.append("")
    lines.extend(["## Poza zakresem tego zadania", ""])
    lines.extend(f"- {item}" for item in plan.out_of_scope)
    lines.append("")
    if plan.campaigns:
        lines.extend(
            [
                "## Strategie stawek z ostatnich 30 dni",
                "",
                "| Kampania | Typ | Strategia | Cel | Koszt | Konwersje | Wartosc konwersji | ROAS | Utrata budzet | Ocena celu | Stabilnosc | Budzet |",
                "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
            ]
        )
        for campaign in plan.campaigns:
            lines.append(
                f"| {campaign['campaign_name']} | {campaign['channel_type']} | "
                f"{campaign['bidding_strategy_type']} | {campaign['target_label']} | "
                f"{format_money(campaign['cost_30d_micros'], plan.currency_code)} | "
                f"{format_decimal(campaign['conversions_30d'])} | "
                f"{format_decimal(campaign['conversion_value_30d'])} | "
                f"{format_decimal(campaign.get('actual_roas', 0))} | "
                f"{percent_label(campaign.get('search_budget_lost_impression_share'))} | "
                f"{campaign.get('target_assessment', '')} | "
                f"{campaign.get('stability_label', '')} | "
                f"{campaign.get('budget_context', '')} |"
            )
        lines.append("")
    recommendations = bidding_recommendations(plan.campaigns)
    if recommendations:
        lines.extend(
            [
                "## Rekomendacje strategii do decyzji",
                "",
                "| Kampania | Waznosc | Rekomendacja | Powod | Dopasowanie strategii |",
                "| --- | --- | --- | --- | --- |",
            ]
        )
        for campaign in recommendations:
            recommendation = campaign["bidding_recommendation"]
            lines.append(
                f"| {campaign['campaign_name']} | {recommendation['level']} | "
                f"{recommendation['action']} | {recommendation['reason']} | "
                f"{campaign.get('strategy_fit', '')} |"
            )
        lines.append("")
    if plan.target_changes:
        lines.extend(
            [
                "## Zmiany celu strategii do akceptacji",
                "",
                "| Kampania | Strategia | Kierunek | Obecnie | Docelowo | Rzeczywisty ROAS | Powod |",
                "| --- | --- | --- | --- | --- | --- | --- |",
            ]
        )
        for change in plan.target_changes:
            lines.append(
                f"| {change['campaign_name']} | {change['strategy']} | {change['direction']} | "
                f"{change['current_label']} | {change['target_label']} | "
                f"{format_decimal(change['actual_roas'])} | {change['reason']} |"
            )
        lines.append("")
    if plan.knowledge_rules:
        lines.extend(
            [
                "## Reguly z bazy wiedzy",
                "",
                "| ID | Temat | Rekomendacja | Ryzyko |",
                "| --- | --- | --- | --- |",
            ]
        )
        for rule in plan.knowledge_rules:
            lines.append(
                f"| {rule.get('id', '')} | {rule.get('topic', '')} | "
                f"{rule.get('recommendation', '')} | {rule.get('risk', '')} |"
            )
        lines.append("")
    md_path.write_text("\n".join(lines), encoding="utf-8")
    return json_path, md_path


def print_bidding_strategy_plan(plan: BiddingStrategyPlan) -> None:
    print("\nPlan sprawdzenia strategii stawek")
    print_table(
        ["Metryka", "Liczba"],
        [
            ["Kampanie z danymi 30 dni", str(len(plan.campaigns))],
            ["Reguly wiedzy", str(len(plan.knowledge_rules))],
            ["Rekomendacje strategii", str(len(bidding_recommendations(plan.campaigns)))],
            ["Zmiany celu do wdrozenia", str(len(plan.target_changes or []))],
        ],
    )
    if plan.warnings:
        print("\nUwagi")
        print_table(["Nr", "Uwaga"], [[str(index), item] for index, item in enumerate(plan.warnings, 1)])
    print("\nZakres zadania")
    print_table(
        ["Nr", "Obszar", "Co sprawdzic"],
        [[str(index), row["area"], row["check"]] for index, row in enumerate(plan.scope, 1)],
    )
    print("\nPoza zakresem")
    print_table(["Nr", "Nie analizujemy tutaj"], [[str(index), item] for index, item in enumerate(plan.out_of_scope, 1)])
    if plan.campaigns:
        print("\nStrategie stawek z ostatnich 30 dni")
        print_table(
            ["Nr", "Kampania", "Typ", "Strategia", "Cel", "Konw.", "ROAS", "Utrata budz.", "Ocena celu", "Budzet"],
            [
                [
                    str(index),
                    campaign["campaign_name"],
                    campaign["channel_type"],
                    campaign["bidding_strategy_type"],
                    campaign["target_label"],
                    format_decimal(campaign["conversions_30d"]),
                    format_decimal(campaign.get("actual_roas", 0)),
                    percent_label(campaign.get("search_budget_lost_impression_share")),
                    campaign.get("target_assessment", ""),
                    campaign.get("budget_context", ""),
                ]
                for index, campaign in enumerate(plan.campaigns, 1)
            ],
        )
    if plan.target_changes:
        print("\nZmiany celu strategii do akceptacji")
        print_table(
            ["Nr", "Kampania", "Strategia", "Kierunek", "Obecnie", "Docelowo", "ROAS", "Powod"],
            [
                [
                    str(index),
                    change["campaign_name"],
                    change["strategy"],
                    change["direction"],
                    change["current_label"],
                    change["target_label"],
                    format_decimal(change["actual_roas"]),
                    change["reason"],
                ]
                for index, change in enumerate(plan.target_changes, 1)
            ],
        )
    recommendations = bidding_recommendations(plan.campaigns)
    if recommendations:
        print("\nRekomendacje strategii do decyzji")
        print_table(
            ["Nr", "Kampania", "Waznosc", "Rekomendacja", "Powod", "Dopasowanie"],
            [
                [
                    str(index),
                    campaign["campaign_name"],
                    campaign["bidding_recommendation"]["level"],
                    campaign["bidding_recommendation"]["action"],
                    campaign["bidding_recommendation"]["reason"],
                    campaign.get("strategy_fit", ""),
                ]
                for index, campaign in enumerate(recommendations, 1)
            ],
        )
    if plan.knowledge_rules:
        print("\nReguly z bazy wiedzy")
        print_table(
            ["Nr", "ID", "Temat", "Rekomendacja"],
            [
                [str(index), rule["id"], rule["topic"], rule["recommendation"]]
                for index, rule in enumerate(plan.knowledge_rules[:10], 1)
            ],
        )
        if len(plan.knowledge_rules) > 10:
            print(f"... oraz {len(plan.knowledge_rules) - 10} kolejnych regul")


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_bidding_strategy_plan(
    client_config: ClientConfig,
    plan: BiddingStrategyPlan,
    show_navigation: bool = True,
) -> None:
    target_changes = plan.target_changes or []
    changed = 0
    errors = []
    if target_changes:
        google_client = get_google_ads_client(use_proto_plus=True)
        customer_id = client_config.safe_customer_id
        service = google_client.get_service("CampaignService")
        operations = []
        for change in target_changes:
            op = google_client.get_type("CampaignOperation")
            campaign = op.update
            campaign.resource_name = service.campaign_path(customer_id, change["campaign_id"])
            field = change["field"]
            if field == "target_roas.target_roas":
                campaign.target_roas.target_roas = float(change["target_roas"])
            elif field == "maximize_conversion_value.target_roas":
                campaign.maximize_conversion_value.target_roas = float(change["target_roas"])
            else:
                errors.append(f"Nieobslugiwane pole celu: {field}")
                continue
            op.update_mask = field_mask_pb2.FieldMask(paths=[field])
            operations.append(op)

        if operations:
            try:
                response = service.mutate_campaigns(customer_id=customer_id, operations=operations)
                changed = len(response.results)
            except Exception as exc:
                errors.append(str(exc))

    if target_changes:
        print("\nWynik wdrozenia zmian celu strategii")
        print(f"Zmieniono kampanii: {changed}")
        print(f"Bledy: {len(errors)}")
        for error in errors:
            print(f"Blad: {error}")
    else:
        print("\nTo zadanie jest audytem strategii stawek i nie ma zmian celu do wdrozenia.")

    rows = [
        {
            "klient": client_config.domain,
            "kampania": change.get("campaign_name", ""),
            "czynnosc": f"Zmien Docelowy ROAS: {change.get('current_label', '')} -> {change.get('target_label', '')}",
            "grupa reklam": "",
            "produkt": change.get("reason", ""),
        }
        for change in target_changes
    ]
    changes_path = append_change_markdown(client_config.domain, TASK_NAME, rows)
    history_path = append_history(
        client_config.domain,
        {
            "task": TASK_NAME,
            "status": "wdrozono zmiany celu strategii" if target_changes and not errors else "audyt oznaczony jako wykonany",
            "campaign": ", ".join(change.get("campaign_name", "") for change in target_changes)
            or ", ".join(campaign["campaign_name"] for campaign in plan.campaigns[:10]),
            "summary": {
                "campaigns": len(plan.campaigns),
                "knowledge_rules": len(plan.knowledge_rules),
                "target_changes": len(target_changes),
                "changed": changed,
                "errors": len(errors),
            },
        },
    )
    print(f"Historia JSONL: {history_path}")
    print(f"Historia Markdown: {changes_path}")
    if show_navigation:
        print_next_navigation(client_config.domain)


def run_check_bidding_strategies(
    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:
    _ = global_rules
    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')}, a wybrano {client_config.domain}.")
            if show_navigation:
                print_next_navigation(client_config.domain)
            return
        plan = BiddingStrategyPlan.from_dict(plan_data)
        print_bidding_strategy_plan(plan)
        apply_bidding_strategy_plan(client_config, plan, show_navigation=show_navigation)
        return

    print(f"\nKlient: {client_config.domain}")
    print("Przygotowuje plan sprawdzenia strategii stawek...")
    plan = build_bidding_strategy_plan(client_config)
    print_bidding_strategy_plan(plan)
    json_path, md_path = save_bidding_strategy_plan(client_config.domain, plan)
    print(f"\nPlan JSON: {json_path}")
    print(f"Plan Markdown: {md_path}")

    append_history(
        client_config.domain,
        {
            "task": TASK_NAME,
            "status": "plan przygotowany",
            "campaign": ", ".join(campaign["campaign_name"] for campaign in plan.campaigns[:10]),
            "summary": {
                "campaigns": len(plan.campaigns),
                "knowledge_rules": len(plan.knowledge_rules),
                "target_changes": len(plan.target_changes or []),
            },
        },
    )

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

    print("\nBrak automatycznego wdrozenia. Uzyj zapisanego planu i potwierdzenia, aby wdrozyc zmiany celu.")
    if show_navigation:
        print_next_navigation(client_config.domain)
