#!/usr/bin/env python3

import copy
import json
import os
import re
import stat
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlparse

state_path = Path(sys.argv[1])
state = json.loads(state_path.read_text(encoding="utf-8"))

now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
listing_pattern = re.compile(r"/listing/([0-9]+)(?:/|$)")
url_pattern = re.compile(r"https?://[^\s<>\"']+")

allowed_hosts = {
    "etsy.com",
    "www.etsy.com",
    "shire3d.etsy.com",
}


def listing_details(value):
    raw = str(value or "").strip()

    if not raw:
        raise ValueError("missing Etsy listing URL")

    parsed = urlparse(raw)
    host = (parsed.hostname or "").lower()

    if host not in allowed_hosts:
        raise ValueError(f"unapproved Etsy host: {host or '<none>'}")

    match = listing_pattern.search(parsed.path)

    if not match:
        raise ValueError(f"missing listing ID in {raw}")

    listing_id = match.group(1)
    customer_url = f"https://shire3d.etsy.com/listing/{listing_id}"

    return raw, listing_id, customer_url


def replace_etsy_urls(text, expected_listing_id, customer_url):
    source = str(text or "")

    def replacement(match):
        complete = match.group(0)
        trailing = ""

        while complete and complete[-1] in ".,;:!?)":
            trailing = complete[-1] + trailing
            complete = complete[:-1]

        try:
            parsed = urlparse(complete)
        except Exception:
            return match.group(0)

        host = (parsed.hostname or "").lower()

        if host not in allowed_hosts:
            return match.group(0)

        listing_match = listing_pattern.search(parsed.path)

        if not listing_match:
            return match.group(0)

        if listing_match.group(1) != expected_listing_id:
            raise ValueError(
                "post contained an Etsy URL for a different listing"
            )

        return customer_url + trailing

    return url_pattern.sub(replacement, source)


products = state.get("products") or []
posts = state.get("posts") or []
queue = state.get("queue") or []
campaigns = state.get("campaigns") or []

product_map = {}
listing_ids = set()

for product in products:
    original_candidate = (
        product.get("originalListingUrl")
        or product.get("listingUrl")
        or product.get("ebayListing")
    )

    original, listing_id, customer_url = listing_details(
        original_candidate
    )

    if listing_id in listing_ids:
        raise ValueError(f"duplicate listing ID: {listing_id}")

    listing_ids.add(listing_id)

    product["originalListingUrl"] = original
    product["customerListingUrl"] = customer_url
    product["shareAndSaveUrl"] = customer_url
    product["shareAndSaveListingId"] = listing_id
    product["linkPolicy"] = "etsy_share_and_save_v1"
    product["linkUpdatedAt"] = now

    product_map[str(product.get("id"))] = {
        "original": original,
        "listing_id": listing_id,
        "customer_url": customer_url,
    }

if len(products) != 88 or len(listing_ids) != 88:
    raise ValueError(
        "expected 88 unique products during migration"
    )

changed_posts = 0
changed_campaign_ids = set()

for post in posts:
    product_id = str(post.get("productId"))
    details = product_map.get(product_id)

    if not details:
        raise ValueError(
            f"post references unknown product: {product_id}"
        )

    before_caption = str(post.get("caption") or "")
    before_listing = str(post.get("listingUrl") or "")
    before_call_to_action = str(post.get("callToAction") or "")

    after_caption = replace_etsy_urls(
        before_caption,
        details["listing_id"],
        details["customer_url"],
    )

    after_call_to_action = replace_etsy_urls(
        before_call_to_action,
        details["listing_id"],
        details["customer_url"],
    )

    changed = (
        after_caption != before_caption
        or after_call_to_action != before_call_to_action
        or before_listing != details["customer_url"]
        or post.get("originalListingUrl") != details["original"]
        or post.get("customerListingUrl") != details["customer_url"]
    )

    if not changed:
        continue

    history = post.setdefault("linkRevisionHistory", [])

    history.append({
        "revision": post.get("revision"),
        "changedAt": now,
        "reason": "etsy_share_and_save_v1",
        "previousCaption": before_caption,
        "previousCallToAction": before_call_to_action,
        "previousListingUrl": before_listing,
        "previousFrozen": post.get("frozen"),
        "previousFrozenSnapshot": copy.deepcopy(
            post.get("frozenSnapshot")
        ),
        "originalDestinationUrl": details["original"],
        "customerDestinationUrl": details["customer_url"],
    })

    post["caption"] = after_caption
    post["callToAction"] = after_call_to_action
    post["originalListingUrl"] = details["original"]
    post["customerListingUrl"] = details["customer_url"]
    post["listingUrl"] = details["customer_url"]
    post["linkPolicy"] = "etsy_share_and_save_v1"
    post["linkUpdatedAt"] = now
    post["revision"] = int(post.get("revision") or 0) + 1
    post["approvalStatus"] = "ray_review"
    post["frozen"] = False
    post["frozenSnapshot"] = None
    post["updatedAt"] = now

    changed_posts += 1
    changed_campaign_ids.add(str(post.get("campaignId")))

post_map = {
    str(post.get("id")): post
    for post in posts
}

sandbox_reset = 0

for item in queue:
    post = post_map.get(str(item.get("postId")))

    if not post:
        raise ValueError(
            f"queue references missing post: {item.get('postId')}"
        )

    item["captionFrozen"] = post.get("caption") or ""
    item["linkPolicy"] = "etsy_share_and_save_v1"
    item["requiresRayReview"] = True
    item["updatedAt"] = now

    published_link = str(item.get("publishedLink") or "")

    if "sandbox.instagram.example" in published_link.lower():
        item["status"] = "queued"
        item["attemptCount"] = 0
        item["publishedLink"] = ""
        item["lastError"] = None
        item.pop("publishedAt", None)
        sandbox_reset += 1

for campaign in campaigns:
    campaign_id = str(campaign.get("id"))

    if campaign_id not in changed_campaign_ids:
        continue

    history = campaign.setdefault("linkRevisionHistory", [])

    history.append({
        "changedAt": now,
        "reason": "etsy_share_and_save_v1",
        "previousStatus": campaign.get("status"),
        "previousApprovedAt": campaign.get("approvedAt"),
        "previousApprovedBy": campaign.get("approvedBy"),
        "previousApprovedVersion": campaign.get("approvedVersion"),
    })

    if campaign.get("status") == "scheduled":
        campaign["status"] = "needs_reapproval"
    elif campaign.get("status") != "ray_review":
        campaign["status"] = "ray_review"

    campaign["approvedAt"] = None
    campaign["approvedBy"] = None
    campaign["approvedVersion"] = None
    campaign["linkPolicy"] = "etsy_share_and_save_v1"
    campaign["linkUpdatedAt"] = now
    campaign["updatedAt"] = now

state["linkPolicy"] = {
    "schemaVersion": 1,
    "mode": "etsy_share_and_save",
    "customerBaseUrl": "https://shire3d.etsy.com/listing/",
    "approvedDestinationHosts": [
        "shire3d.etsy.com",
    ],
    "approvedSourceHosts": sorted(allowed_hosts),
    "preserveOriginalDestination": True,
    "rayApprovalRequired": True,
    "futureBrandedRedirectSupported": True,
    "updatedAt": now,
}

state.setdefault("linkMigrationHistory", []).append({
    "schemaVersion": 1,
    "completedAt": now,
    "policy": "etsy_share_and_save_v1",
    "productsUpdated": len(products),
    "postsUpdated": changed_posts,
    "queueEntriesUpdated": len(queue),
    "sandboxReceiptsReset": sandbox_reset,
    "originalDestinationsPreserved": True,
    "publishingEnabled": False,
})

if changed_posts != len(posts):
    raise ValueError(
        f"expected to revise {len(posts)} posts, revised {changed_posts}"
    )

if sandbox_reset != 1:
    raise ValueError(
        f"expected exactly one sandbox receipt, found {sandbox_reset}"
    )

for post in posts:
    listing_url = str(post.get("listingUrl") or "")

    if not re.fullmatch(
        r"https://shire3d\.etsy\.com/listing/[0-9]+",
        listing_url,
    ):
        raise ValueError(
            f"post has invalid customer listing URL: {post.get('id')}"
        )

    if "www.etsy.com" in str(post.get("caption") or ""):
        raise ValueError(
            f"post still contains www.etsy.com: {post.get('id')}"
        )

for item in queue:
    post = post_map[str(item.get("postId"))]

    if item.get("captionFrozen") != post.get("caption"):
        raise ValueError(
            f"queue caption mismatch: {item.get('id')}"
        )

published_count = sum(
    1 for item in queue
    if item.get("status") == "published"
)

if published_count != 0:
    raise ValueError(
        f"unexpected published queue entries remain: {published_count}"
    )

before = state_path.stat()

fd, temporary = tempfile.mkstemp(
    prefix=f".{state_path.name}.",
    suffix=".tmp",
    dir=str(state_path.parent),
)

try:
    with os.fdopen(fd, "w", encoding="utf-8") as handle:
        json.dump(
            state,
            handle,
            ensure_ascii=False,
            indent=2,
        )
        handle.write("\n")
        handle.flush()
        os.fsync(handle.fileno())

    os.chmod(temporary, stat.S_IMODE(before.st_mode))
    os.replace(temporary, state_path)
finally:
    if os.path.exists(temporary):
        os.unlink(temporary)

print("Products updated:", len(products))
print("Posts updated:", changed_posts)
print("Queue entries updated:", len(queue))
print("Sandbox receipts reset:", sandbox_reset)
print("PASS: State migration completed atomically.")
