#!/usr/bin/env python3
# Author: Tom Sapletta · https://tom.sapletta.com
# Part of the ifURI solution.

"""Build contractor metadata from invoice OCR/report manifests."""

from __future__ import annotations

import argparse
import csv
import json
import re
from pathlib import Path
from typing import Any

BRANDS: list[tuple[str, str, str, str]] = [
    ("allegro", "Allegro", "marketplace", "PL"),
    ("openai", "OpenAI", "ai-saas", "US"),
    ("anthropic", "Anthropic", "ai-saas", "US"),
    ("github", "GitHub", "developer-saas", "US"),
    ("microsoft", "Microsoft", "software", "US"),
    ("google", "Google", "software", "US"),
    ("cloudflare", "Cloudflare", "infrastructure", "US"),
    ("ovh", "OVHcloud", "infrastructure", "FR"),
    ("home.pl", "home.pl", "hosting", "PL"),
    ("nazwa.pl", "nazwa.pl", "hosting", "PL"),
    ("hostinger", "Hostinger", "hosting", "LT"),
    ("digitalocean", "DigitalOcean", "infrastructure", "US"),
    ("hetzner", "Hetzner", "infrastructure", "DE"),
    ("aws", "Amazon Web Services", "infrastructure", "US"),
    ("amazon web services", "Amazon Web Services", "infrastructure", "US"),
    ("stripe", "Stripe", "payments", "US"),
    ("paypal", "PayPal", "payments", "US"),
    ("paddle", "Paddle", "payments", "GB"),
    ("fastspring", "FastSpring", "payments", "US"),
    ("jetbrains", "JetBrains", "developer-saas", "CZ"),
    ("cursor", "Cursor", "ai-saas", "US"),
    ("windsurf", "Windsurf", "ai-saas", "US"),
    ("codeium", "Windsurf", "ai-saas", "US"),
    ("elevenlabs", "ElevenLabs", "ai-saas", "US"),
    ("midjourney", "Midjourney", "ai-saas", "US"),
    ("canva", "Canva", "design-saas", "AU"),
    ("figma", "Figma", "design-saas", "US"),
    ("notion", "Notion", "productivity-saas", "US"),
    ("slack", "Slack", "productivity-saas", "US"),
    ("zoom", "Zoom", "productivity-saas", "US"),
    ("linkedin", "LinkedIn", "social", "US"),
    ("facebook", "Meta", "ads-social", "US"),
    ("meta", "Meta", "ads-social", "US"),
    ("ikea", "IKEA", "retail", "SE"),
    ("skynova", "SkyNova Krystian Mańczyna", "poland", "PL"),
    ("premium.pl", "premium.pl Sp. z o.o.", "domains-hosting", "PL"),
    ("inpost", "InPost sp. z o.o.", "logistics", "PL"),
    ("botland", "Botland", "electronics", "PL"),
    ("cyf-rowe", "CyF-rowe", "electronics", "PL"),
    ("zbro-trans", "ZBRO-TRANS Michał Zbrożek", "transport", "PL"),
    ("grzegorz franaszek", "GRZEGORZ FRANASZEK F.H.U. EFEKT", "services", "PL"),
]

PRODUCT_RULES: list[tuple[str, str, str]] = [
    ("windsurf", "AI coding assistant subscription", "known-service"),
    ("codeium", "AI coding assistant subscription", "known-service"),
    ("openai", "AI model/API subscription", "known-service"),
    ("anthropic", "AI model/API subscription", "known-service"),
    ("elevenlabs", "AI voice generation service", "known-service"),
    ("midjourney", "AI image generation subscription", "known-service"),
    ("github", "Developer platform subscription", "known-service"),
    ("jetbrains", "Developer tools subscription", "known-service"),
    ("cursor", "AI coding editor subscription", "known-service"),
    ("figma", "Design collaboration subscription", "known-service"),
    ("canva", "Design/content creation subscription", "known-service"),
    ("notion", "Productivity workspace subscription", "known-service"),
    ("slack", "Team communication subscription", "known-service"),
    ("zoom", "Video conferencing subscription", "known-service"),
    ("linkedin", "Social/business platform service", "known-service"),
    ("facebook", "Advertising/social platform service", "known-service"),
    ("meta", "Advertising/social platform service", "known-service"),
    ("allegro", "Marketplace purchase", "known-service"),
    ("amazon web services", "Cloud infrastructure usage", "known-service"),
    ("aws", "Cloud infrastructure usage", "known-service"),
    ("ovh", "Hosting/cloud infrastructure service", "known-service"),
    ("hetzner", "Hosting/cloud infrastructure service", "known-service"),
    ("digitalocean", "Cloud infrastructure service", "known-service"),
    ("cloudflare", "DNS/CDN/security service", "known-service"),
    ("premium.pl", "Domain registration/hosting service", "known-service"),
    ("home.pl", "Domain/hosting service", "known-service"),
    ("nazwa.pl", "Domain/hosting service", "known-service"),
    ("hostinger", "Domain/hosting service", "known-service"),
    ("inpost", "Shipping/logistics service", "known-service"),
    ("botland", "Electronics parts/supplies", "known-service"),
    ("ikea", "Retail goods/service", "known-service"),
    ("paypal", "Payment processing service", "known-service"),
    ("stripe", "Payment processing service", "known-service"),
    ("paddle", "Payment processing service", "known-service"),
    ("fastspring", "Payment processing service", "known-service"),
]

BUYER_PATTERNS = (
    "Tomasz Sapletta",
    "prototypowanie.pl",
)

GENERIC_LABELS = {
    "",
    "unknown",
    "unknown-review",
    "review",
    "ai-tools",
    "ai tools",
    "ai-saas",
    "ai saas",
    "bank-finance",
    "bank finance",
    "email-assets",
    "email assets",
    "email-assets-review",
    "email assets review",
    "domains-hosting",
    "domains hosting",
    "payments",
    "poland",
    "ksef-gov",
    "ksef gov",
}

SUPPLIER_MARKERS = (
    "sprzedawca",
    "sprzedający",
    "wystawca",
    "seller",
    "supplier",
    "vendor",
    "merchant",
    "billed by",
)

BUYER_MARKERS = (
    "nabywca",
    "kupujący",
    "odbiorca",
    "buyer",
    "bill to",
    "billed to",
    "customer",
)

STOP_MARKERS = SUPPLIER_MARKERS + BUYER_MARKERS + (
    "faktura",
    "invoice",
    "data wystawienia",
    "invoice date",
    "vat",
    "nip",
)

PRODUCT_MARKERS = (
    "nazwa towaru/usługi",
    "nazwa towaru",
    "nazwa usługi",
    "opis usługi",
    "item description",
    "description:",
    "description ",
    "subscription",
    "plan:",
)


def normalize_text(value: str) -> str:
    return " ".join(str(value or "").replace("\x03", " ").split())


def slug_label(value: str) -> str:
    return " ".join(part for part in re.split(r"[-_/]+", value) if part)


def load_csv(path: Path) -> list[dict[str, str]]:
    if not path.is_file():
        return []
    with path.open(encoding="utf-8", newline="") as handle:
        return list(csv.DictReader(handle))


def load_json_rows(path: Path) -> list[dict[str, Any]]:
    if not path.is_file():
        return []
    data = json.loads(path.read_text(encoding="utf-8"))
    if isinstance(data, dict):
        for key in ("rows", "files", "results"):
            if isinstance(data.get(key), list):
                return [row for row in data[key] if isinstance(row, dict)]
    if isinstance(data, list):
        return [row for row in data if isinstance(row, dict)]
    return []


def build_target_map(state_dir: Path) -> dict[str, str]:
    mapping: dict[str, str] = {}
    for stem in (
        "flat_invoice_manifest",
        "flat_tree_manifest",
        "flat_date_repair_manifest",
        "no_invoice_move_manifest",
        "invoice_recheck_manifest",
        "invoice_recheck_plan",
    ):
        for row in [*load_csv(state_dir / f"{stem}.csv"), *load_json_rows(state_dir / f"{stem}.json")]:
            source = normalize_path(str(row.get("path", "")))
            target = normalize_path(str(row.get("target", "")))
            if source and target:
                mapping[source] = target
                mapping[target] = target
            elif source:
                mapping[source] = source
    return mapping


def normalize_path(path: str) -> str:
    path = path.replace("\\", "/").strip("/")
    parts = [part for part in path.split("/") if part and part not in {".", ".."}]
    return "/".join(parts)


def tax_id(text: str) -> str:
    patterns = [
        r"\b(?:NIP[:\s]*)?([0-9]{3}[- ]?[0-9]{3}[- ]?[0-9]{2}[- ]?[0-9]{2})\b",
        r"\b(PL[0-9]{10})\b",
        r"\b(VAT[:\s]*(?:ID[:\s]*)?[A-Z]{2}[A-Z0-9]{8,14})\b",
    ]
    for pattern in patterns:
        match = re.search(pattern, text, flags=re.I)
        if match:
            return re.sub(r"[\s-]+", "", match.group(1).upper())
    return ""


def known_buyer(text: str) -> str:
    lowered = text.lower()
    hits = [name for name in BUYER_PATTERNS if name.lower() in lowered]
    return " / ".join(hits)


def stop_at_marker(value: str) -> str:
    lowered = value.lower()
    indexes = [lowered.find(marker) for marker in STOP_MARKERS if lowered.find(marker) > 0]
    if indexes:
        value = value[: min(indexes)]
    return normalize_text(value)


def clean_party(value: str) -> str:
    value = re.sub(r"\b(SPRZEDAWCA|NABYWCA|SELLER|BUYER|SUPPLIER|CUSTOMER|BILL TO|BILLED TO)\b\s*:?", "", value, flags=re.I)
    value = re.sub(r"\b(NIP|VAT|TAX ID)\b.*", "", value, flags=re.I)
    value = normalize_text(value)
    value = re.sub(r"^[.:;\-\s]+", "", value)
    lowered = value.lower()
    if lowered.startswith(("/podatnik", "nabyw", "kupuj", "nr / no")):
        return ""
    return value[:120]


def party_after_marker(text: str, markers: tuple[str, ...]) -> str:
    lowered = text.lower()
    for marker in markers:
        index = lowered.find(marker)
        if index < 0:
            continue
        segment = text[index + len(marker) : index + len(marker) + 220]
        candidate = clean_party(stop_at_marker(segment))
        if candidate and len(candidate) >= 3:
            return candidate
    return ""


def known_brand(path: str, text: str, label: str = "") -> tuple[str, str, str, str]:
    haystack = f"{path}\n{label}\n{text}".lower()
    for token, name, platform, country in BRANDS:
        if token in haystack:
            return name, platform, country, token
    return "", "", "", ""


def clean_product(value: str) -> str:
    value = normalize_text(value)
    value = re.sub(r"\b(ilość|ilosc|qty|quantity|netto|brutto|vat|amount|total|razem|suma)\b.*", "", value, flags=re.I)
    value = re.sub(r"^[.:;\-\s]+", "", value)
    value = re.sub(r"\s{2,}", " ", value)
    value = value[:100].strip()
    lowered = value.lower()
    bad_fragments = (
        "obrońców",
        "obroncow",
        "ul.",
        "szemud",
        "corporate profile",
        "empowering brands",
        "adres",
        "address",
    )
    if any(fragment in lowered for fragment in bad_fragments):
        return ""
    return value


def product_after_marker(text: str) -> str:
    lowered = text.lower()
    for marker in PRODUCT_MARKERS:
        index = lowered.find(marker)
        if index < 0:
            continue
        segment = text[index + len(marker) : index + len(marker) + 180]
        candidate = clean_product(segment)
        if len(candidate) >= 4:
            return candidate
    return ""


def known_products(path: str, text: str, contractor: str, platform: str, label: str, category: str) -> tuple[list[str], str]:
    haystack = f"{path}\n{text}\n{contractor}\n{platform}\n{label}\n{category}".lower()
    products: list[str] = []
    source = ""
    for token, product, product_source in PRODUCT_RULES:
        if token in haystack and product not in products:
            products.append(product)
            source = product_source
    if not products:
        marker_product = product_after_marker(text)
        if marker_product:
            products.append(marker_product)
            source = "ocr-marker"
    if not products and platform:
        fallback = {
            "ai-saas": "AI/SaaS subscription",
            "developer-saas": "Developer SaaS subscription",
            "design-saas": "Design SaaS subscription",
            "productivity-saas": "Productivity SaaS subscription",
            "domains-hosting": "Domain/hosting service",
            "hosting": "Hosting service",
            "infrastructure": "Infrastructure service",
            "payments": "Payment service",
            "marketplace": "Marketplace purchase",
            "logistics": "Shipping/logistics service",
            "electronics": "Electronics purchase",
            "retail": "Retail purchase",
        }.get(platform)
        if fallback:
            products.append(fallback)
            source = "platform"
    return products[:4], source


def label_fallback(path: str, label: str, category: str) -> str:
    if label and label.lower() not in GENERIC_LABELS:
        return slug_label(label)
    match = re.match(r"^(?:no_invoice/)?20\d{2}[.-]\d{2}/20\d{2}[.-]\d{2}[.-]\d{2}-([^-]+(?:-[^-]+)?)-", path)
    if match:
        candidate = slug_label(match.group(1))
        lowered = candidate.lower()
        if lowered in GENERIC_LABELS or any(lowered.startswith(generic + " ") for generic in GENERIC_LABELS if generic):
            return ""
        return candidate
    if category and category.lower() not in GENERIC_LABELS:
        return slug_label(category)
    return ""


def contractor_from_row(row: dict[str, Any], target: str) -> dict[str, str]:
    preview = normalize_text(str(row.get("preview", "")))
    label = normalize_text(str(row.get("label") or row.get("category") or ""))
    category = normalize_text(str(row.get("category", "")))
    path = target or normalize_path(str(row.get("path", "")))
    contractor, platform, country, evidence = known_brand(path, preview, label)
    source = "known-brand" if contractor else ""
    if not contractor:
        contractor = party_after_marker(preview, SUPPLIER_MARKERS)
        source = "ocr-marker" if contractor else ""
    buyer = known_buyer(preview) or party_after_marker(preview, BUYER_MARKERS)
    nip = tax_id(preview)
    confidence = "0.90" if source == "known-brand" else "0.70" if source == "ocr-marker" else "0.45" if source else "0.00"
    inferred_platform = platform or ("" if category.lower() in {"unknown", "unknown-review"} else category)
    products, product_source = known_products(path, preview, contractor, inferred_platform, label, category)
    return {
        "path": path,
        "contractor": contractor,
        "buyer": buyer,
        "tax_id": nip,
        "country": country,
        "platform": inferred_platform,
        "products_services": "; ".join(products),
        "product_source": product_source,
        "confidence": confidence,
        "source": "contractor_metadata",
        "contractor_source": source,
        "evidence": evidence or (preview[:120] if source == "ocr-marker" else label or category),
    }


def source_rows(state_dir: Path) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    rows.extend(load_json_rows(state_dir / "invoice_audit_raw.json"))
    rows.extend(load_csv(state_dir / "invoice_audit_files.csv"))
    rows.extend(load_csv(state_dir / "invoice_recheck_manifest.csv"))
    rows.extend(load_csv(state_dir / "invoice_recheck_plan.csv"))
    return rows


def build_contractor_metadata(state_dir: Path) -> list[dict[str, str]]:
    mapping = build_target_map(state_dir)
    by_path: dict[str, dict[str, str]] = {}
    for row in source_rows(state_dir):
        source = normalize_path(str(row.get("path", "")))
        if not source:
            continue
        targets = [mapping.get(source, source)]
        row_target = normalize_path(str(row.get("target", "")))
        if row_target:
            targets.append(row_target)
        for target in dict.fromkeys(targets):
            meta = contractor_from_row(row, target)
            if (
                not meta["contractor"]
                and not meta["buyer"]
                and not meta["tax_id"]
                and not meta["platform"]
                and not meta["products_services"]
            ):
                continue
            old = by_path.get(target)
            if old is None or float(meta["confidence"]) >= float(old.get("confidence", "0") or 0):
                by_path[target] = meta
    return sorted(by_path.values(), key=lambda row: row["path"])


def write_csv(path: Path, rows: list[dict[str, str]]) -> None:
    fields = [
        "path",
        "contractor",
        "buyer",
        "tax_id",
        "country",
        "platform",
        "products_services",
        "product_source",
        "confidence",
        "source",
        "contractor_source",
        "evidence",
    ]
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=fields)
        writer.writeheader()
        for row in rows:
            writer.writerow({field: row.get(field, "") for field in fields})


def run(state_dir: Path) -> dict[str, Any]:
    rows = build_contractor_metadata(state_dir)
    csv_path = state_dir / "contractor_metadata.csv"
    json_path = state_dir / "contractor_metadata.json"
    write_csv(csv_path, rows)
    json_path.write_text(json.dumps({"ok": True, "rows": rows, "count": len(rows)}, ensure_ascii=False, indent=2), encoding="utf-8")
    return {"ok": True, "count": len(rows), "csv": str(csv_path), "json": str(json_path)}


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Build contractor metadata from invoice reports.")
    parser.add_argument("--state-dir", default=".state")
    args = parser.parse_args(argv)
    print(json.dumps(run(Path(args.state_dir)), ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
