from __future__ import annotations

import json
import os
import re
import time
from dataclasses import dataclass
from pathlib import Path

import requests

from ..config import ClientConfig, client_dir
from ..history import append_change_markdown, append_history, now_local
from ..table import print_table


TASK_ID = "check_product_availability"
TASK_NAME = "Sprawdzenie dostepnosci produktow"


@dataclass
class ProductAvailabilityPlan:
    products_checked: int
    available: list[dict]
    unavailable: list[dict]
    not_mapped: list[dict]
    changes: list[dict]
    warnings: list[str]

    def to_dict(self) -> dict:
        return {
            "task": TASK_ID,
            "task_name": TASK_NAME,
            "products_checked": self.products_checked,
            "available": self.available,
            "unavailable": self.unavailable,
            "not_mapped": self.not_mapped,
            "changes": self.changes,
            "warnings": self.warnings,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "ProductAvailabilityPlan":
        return cls(
            products_checked=int(data.get("products_checked", 0)),
            available=data.get("available", []),
            unavailable=data.get("unavailable", []),
            not_mapped=data.get("not_mapped", []),
            changes=data.get("changes", []),
            warnings=data.get("warnings", []),
        )


def adspro_credentials(client_config: ClientConfig) -> tuple[str, str, str]:
    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_config.adspro_client_id:
        raise RuntimeError(f"Brak adspro_client_id dla {client_config.domain} w config/clients.toml.")
    return api_url, api_key, client_config.adspro_client_id


def adspro_request(api_url: str, payload: dict, timeout: int = 60) -> dict:
    response = requests.post(api_url, data=payload, timeout=timeout)
    response.raise_for_status()
    response.encoding = "utf-8"
    data = response.json()
    if data.get("result") == "error":
        raise RuntimeError(data.get("message") or "adsPRO zwrocil blad.")
    return data


def fetch_adspro_products(client_config: ClientConfig) -> list[dict]:
    api_url, api_key, adspro_client_id = adspro_credentials(client_config)
    data = adspro_request(
        api_url,
        {
            "action": "products_get_all",
            "api_key": api_key,
            "client_id": adspro_client_id,
            "limit": "5000",
        },
    )
    return data.get("products", [])


def fetch_shopify_catalog(shop_url: str) -> list[dict]:
    base = shop_url.rstrip("/")
    headers = {"User-Agent": "Mozilla/5.0", "Accept": "application/json"}
    catalog = []
    for page in range(1, 100):
        url = f"{base}/products.json?limit=250&page={page}"
        response = None
        for attempt in range(1, 4):
            response = requests.get(url, timeout=30, headers=headers)
            if response.status_code == 200:
                break
            time.sleep(1.5 * attempt)
        if response is None or response.status_code != 200:
            raise RuntimeError(f"Nie udalo sie pobrac katalogu Shopify: {url}, status={response.status_code if response else 'brak'}.")
        products = response.json().get("products", [])
        if not products:
            break
        catalog.extend(products)
        time.sleep(0.2)
    return catalog


def offer_parts(offer_id: str) -> tuple[str, str] | None:
    parts = offer_id.split("_")
    if len(parts) >= 4 and parts[0] == "shopify":
        return parts[2], parts[3]
    return None


def check_shopify_products(products: list[dict], rules: dict) -> tuple[list[dict], list[dict], list[dict]]:
    shop_url = str(rules.get("shop_url") or "").rstrip("/")
    if not shop_url:
        raise RuntimeError("Brak product_availability.shop_url dla klienta Shopify.")
    catalog = fetch_shopify_catalog(shop_url)
    by_product_id = {str(product["id"]): product for product in catalog}
    by_variant_id = {str(variant["id"]): product for product in catalog for variant in product.get("variants", [])}
    variant_by_id = {str(variant["id"]): variant for product in catalog for variant in product.get("variants", [])}
    by_title = {str(product.get("title") or "").strip().lower(): product for product in catalog}

    available = []
    unavailable = []
    not_mapped = []
    for product in products:
        offer_id = str(product.get("offer_id") or "").strip()
        matched_product = None
        matched_variant = None
        match_type = "none"
        parts = offer_parts(offer_id)
        if parts:
            product_id, variant_id = parts
            matched_product = by_product_id.get(product_id) or by_variant_id.get(variant_id)
            matched_variant = variant_by_id.get(variant_id)
            match_type = "shopify_id" if matched_product else "none"
        if matched_product is None:
            matched_product = by_title.get(str(product.get("title") or "").strip().lower())
            if matched_product:
                match_type = "title"
        if matched_product and matched_variant is None:
            variants = matched_product.get("variants") or []
            matched_variant = next((variant for variant in variants if variant.get("available")), None) or (variants[0] if variants else None)

        row = availability_row(product)
        row.update(
            {
                "method": "shopify_products_json",
                "match": match_type,
                "url": f"{shop_url}/products/{matched_product.get('handle')}" if matched_product else "",
                "shopify_product_id": str(matched_product.get("id")) if matched_product else "",
                "shopify_variant_id": str(matched_variant.get("id")) if matched_variant else "",
                "shopify_variant_available": bool(matched_variant and matched_variant.get("available")),
            }
        )
        if not matched_product or not matched_variant:
            row["availability_status"] = "not_mapped"
            not_mapped.append(row)
        elif matched_variant.get("available"):
            row["availability_status"] = "available"
            available.append(row)
        else:
            row["availability_status"] = "unavailable"
            row["reason"] = "wariant Shopify ma available=false"
            unavailable.append(row)
    return available, unavailable, not_mapped


def check_html_buy_button(products: list[dict], rules: dict) -> tuple[list[dict], list[dict], list[dict]]:
    url_template = str(rules.get("url_template") or "")
    available_text = str(rules.get("available_text") or "DoKoszykaKartaProduktu")
    hidden_text = str(rules.get("hidden_text") or 'id="PrzyciskKupowania" style="display:none"')
    if not url_template:
        raise RuntimeError("Brak product_availability.url_template dla metody html_buy_button.")

    session = requests.Session()
    session.headers.update({"User-Agent": "Mozilla/5.0"})
    available = []
    unavailable = []
    not_mapped = []
    for product in products:
        offer_id = str(product.get("offer_id") or "").strip()
        url = url_template.format(offer_id=offer_id)
        row = availability_row(product)
        row.update({"method": "html_buy_button", "url": url})
        try:
            response = session.get(url, timeout=20, allow_redirects=True)
            response.encoding = "utf-8"
            text = response.text
            lower = text.lower()
            row["http_status"] = response.status_code
            row["final_url"] = response.url
            has_button = available_text in text
            hidden = hidden_text.lower() in lower
            if response.status_code == 404:
                row["availability_status"] = "not_mapped"
                row["reason"] = "strona produktu zwrocila 404"
                not_mapped.append(row)
            elif has_button and not hidden:
                row["availability_status"] = "available"
                available.append(row)
            else:
                row["availability_status"] = "unavailable"
                row["reason"] = "brak aktywnego przycisku Dodaj do koszyka"
                unavailable.append(row)
        except Exception as exc:
            row["availability_status"] = "not_mapped"
            row["reason"] = f"blad odczytu strony: {exc}"
            not_mapped.append(row)
    return available, unavailable, not_mapped


def availability_row(product: dict) -> dict:
    return {
        "offer_id": str(product.get("offer_id") or "").strip(),
        "title": product.get("title") or "",
        "current_custom_label_4": product.get("custom_label_4") or "",
    }


def build_product_availability_plan(client_config: ClientConfig, global_rules: dict) -> ProductAvailabilityPlan:
    rules = client_config.effective_rules(global_rules, "product_availability")
    method = str(rules.get("method") or "").strip()
    if not method:
        raise RuntimeError(
            f"Brak konfiguracji product_availability.method dla klienta {client_config.domain}."
        )
    products = fetch_adspro_products(client_config)
    if method == "shopify_products_json":
        available, unavailable, not_mapped = check_shopify_products(products, rules)
    elif method == "html_buy_button":
        available, unavailable, not_mapped = check_html_buy_button(products, rules)
    else:
        raise RuntimeError(f"Nieznana metoda product_availability: {method}.")

    target_label = str(rules.get("target_unavailable_label") or "paused")
    changes = []
    for row in unavailable:
        if row.get("current_custom_label_4") == target_label:
            continue
        changes.append(
            {
                **row,
                "target_custom_label_4": target_label,
            }
        )

    warnings = []
    if not_mapped:
        warnings.append(f"Produkty niedopasowane albo bez jednoznacznego odczytu: {len(not_mapped)}.")
    if unavailable:
        warnings.append(f"Produkty niedostepne reklamowo: {len(unavailable)}.")

    return ProductAvailabilityPlan(
        products_checked=len(products),
        available=available,
        unavailable=unavailable,
        not_mapped=not_mapped,
        changes=changes,
        warnings=warnings,
    )


def save_plan(domain: str, plan: ProductAvailabilityPlan) -> 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 = [
        f"# Plan: {TASK_NAME}",
        "",
        f"Klient: {domain}",
        f"Utworzono: {ts.isoformat(timespec='seconds')}",
        "",
        "## Podsumowanie",
        "",
        f"- Produkty sprawdzone: {plan.products_checked}",
        f"- Dostepne: {len(plan.available)}",
        f"- Niedostepne reklamowo: {len(plan.unavailable)}",
        f"- Niedopasowane: {len(plan.not_mapped)}",
        f"- Do ustawienia CL4=paused: {len(plan.changes)}",
        "",
    ]
    if plan.warnings:
        lines.extend(["## Uwagi", ""])
        lines.extend(f"- {item}" for item in plan.warnings)
        lines.append("")
    if plan.changes:
        lines.extend(["## Zmiany do wdrozenia", "", "| Produkt | Obecne CL4 | Docelowe CL4 | Powod | URL |", "| --- | --- | --- | --- | --- |"])
        for row in plan.changes:
            lines.append(
                f"| {row['offer_id']} | {row.get('current_custom_label_4', '')} | "
                f"{row.get('target_custom_label_4', '')} | {row.get('reason', '')} | {row.get('url', '')} |"
            )
        lines.append("")
    md_path.write_text("\n".join(lines), encoding="utf-8")
    return json_path, md_path


def print_plan(plan: ProductAvailabilityPlan) -> None:
    print(f"\nPlan: {TASK_NAME}")
    print_table(
        ["Zakres", "Liczba"],
        [
            ["Produkty sprawdzone", str(plan.products_checked)],
            ["Dostepne", str(len(plan.available))],
            ["Niedostepne reklamowo", str(len(plan.unavailable))],
            ["Niedopasowane", str(len(plan.not_mapped))],
            ["Do ustawienia CL4=paused", str(len(plan.changes))],
        ],
    )
    if plan.changes:
        print("\nNajwazniejsze dzialania")
        rows = [
            [str(index), row["offer_id"], row.get("current_custom_label_4", ""), row.get("target_custom_label_4", ""), row.get("title", "")]
            for index, row in enumerate(plan.changes[:10], 1)
        ]
        print_table(["Nr", "Produkt", "Obecne CL4", "Docelowe CL4", "Tytul"], rows)
        if len(plan.changes) > 10:
            print(f"... oraz {len(plan.changes) - 10} kolejnych produktow")
    for warning in plan.warnings:
        print(f"Uwaga: {warning}")


def set_cl4(api_url: str, api_key: str, client_id: str, offer_id: str, value: str) -> dict:
    return adspro_request(
        api_url,
        {
            "action": "product_custom_label_4_set",
            "api_key": api_key,
            "client_id": client_id,
            "offer_id": offer_id,
            "custom_label_4": value,
        },
        timeout=30,
    )


def apply_plan(client_config: ClientConfig, plan: ProductAvailabilityPlan, plan_path: str) -> None:
    api_url, api_key, adspro_client_id = adspro_credentials(client_config)
    applied = []
    errors = []
    for row in plan.changes:
        try:
            set_cl4(api_url, api_key, adspro_client_id, row["offer_id"], row["target_custom_label_4"])
            applied.append(row)
        except Exception as exc:
            errors.append({**row, "error": str(exc)})
            print(f"Blad zapisu {row['offer_id']}: {exc}")

    print("\nWynik wdrozenia zmian")
    print(f"Wdrozono zmian: {len(applied)}")
    print(f"Bledy: {len(errors)}")

    change_rows = [
        {
            "klient": client_config.domain,
            "produkt": row["offer_id"],
            "pole": "custom_label_4",
            "obecnie": row.get("current_custom_label_4", ""),
            "docelowo": row.get("target_custom_label_4", ""),
        }
        for row in applied
    ]
    changes_path = append_change_markdown(client_config.domain, TASK_NAME, change_rows)
    history_path = append_history(
        client_config.domain,
        {
            "task": TASK_NAME,
            "status": "wdrozono zmiany" if not errors else "wdrozono czesciowo",
            "product": ", ".join(row["offer_id"] for row in applied[:10]),
            "plan_path": plan_path,
            "summary": {"applied": len(applied), "errors": len(errors)},
        },
    )
    print(f"Historia JSONL: {history_path}")
    print(f"Historia Markdown: {changes_path}")


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 run_check_product_availability(
    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
        data = json.loads(Path(apply_plan_path).read_text(encoding="utf-8"))
        if data.get("client") != client_config.domain:
            print(f"Plan jest dla klienta {data.get('client')}, a wybrano {client_config.domain}.")
            if show_navigation:
                print_next_navigation(client_config.domain)
            return
        plan = ProductAvailabilityPlan.from_dict(data)
        print_plan(plan)
        apply_plan(client_config, plan, apply_plan_path)
        if show_navigation:
            print_next_navigation(client_config.domain)
        return

    print(f"\nKlient: {client_config.domain}")
    print("Sprawdzam dostepnosc produktow i przygotowuje plan CL4=paused...")
    plan = build_product_availability_plan(client_config, global_rules)
    print_plan(plan)
    json_path, md_path = save_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",
            "summary": {
                "products_checked": plan.products_checked,
                "available": len(plan.available),
                "unavailable": len(plan.unavailable),
                "not_mapped": len(plan.not_mapped),
                "changes": len(plan.changes),
            },
        },
    )
    if plan_only:
        print("\nTryb plan-only: zmiany nie zostaly wdrozone.")
    if show_navigation:
        print_next_navigation(client_config.domain)
