"""Assemble the accounting-nuxt gold seed (non-cutover) from sealed hosts + adapters.

Writes a sanitizer-clean Nuxt tree that mounts crm.customer, billing.invoice, and
reporting.dashboard. Does not touch GitHub. Does not overwrite starter/main.
"""
from __future__ import annotations

import argparse
import json
import shutil
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
BOILER = ROOT / "boilerplates" / "nuxt_app"
REF = "accounting-nuxt"

_BLOCK_HOSTS = {
    "AdminRecordDetail.vue",
    "AdminRecordTable.vue",
    "CustomerCreate.vue",
    "CustomerDetail.vue",
    "CustomerEdit.vue",
    "CustomerTable.vue",
    "InvoiceCreate.vue",
    "InvoiceDetail.vue",
    "InvoiceEdit.vue",
    "InvoiceTable.vue",
    "ReportCreate.vue",
    "ReportDetail.vue",
    "ReportEdit.vue",
    "ReportTable.vue",
}

_SCHEMAS = ("customer.json", "invoice_record.json", "report_definition.json")

_GITIGNORE = """\
.output
.amplify-hosting
.data
.nuxt
.nitro
.cache
dist
node_modules
logs
*.log
.DS_Store
.fleet
.idea
.cursor
.cursorrules
.P2HS
.env
.env.*
!.env.example
.dev.vars
.p2hs.json
.p2hs/
.p2hs-control/
CLAUDE.md
AGENTS.md
p2hs.platform.json
*.p2hs-local.bak
.p2hs/local-bak/
.history
"""

_README = """\
# LedgerFlow gold seed (starter-v2)

Architecture test for `accounting-nuxt`. This is **not** a general ledger and
**not** checkout. Invoice `paid` is a record status. Customers, invoices, and
saved report definitions use admitted App Blocks on relative `/api`.

- `development` — Cloudflare Pages test (`https://dev-accounting-nuxt.p2hsapp.com/`)
- `starter-v2` — clone candidate
- `starter` / `main` — unchanged until this deploy is proven

Browser source never embeds a Cloud or Core origin.
"""

_CLOUD_ACCESS = """\
# Cloud access

The browser calls relative `/api` only. Server-side BFF adapters bind
`CLOUD_GATEWAY_URL` and `CLOUD_SERVICE_TOKEN`. Do not add public gateway or
Core URLs to client env.
"""

_ENV_EXAMPLE = """\
# Dev-only Nitro proxy target (server config, not public).
NUXT_P2HS_GATEWAY_PROXY_TARGET=http://127.0.0.1:8787

# Browser-safe IDs. Do not add a public Cloud or Core URL.
# NUXT_PUBLIC_P2HS_CLIENT_ID=
# NUXT_PUBLIC_P2HS_PROJECT_ID=
"""


def _write(path: Path, content: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8", newline="\n")


def _copy_file(src: Path, dest: Path) -> None:
    dest.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(src, dest)


def _copy_dir_filtered(src: Path, dest: Path, *, allowed_names: set[str] | None = None) -> None:
    if not src.is_dir():
        raise FileNotFoundError(src)
    for path in src.iterdir():
        if path.name in {".git", "node_modules", ".nuxt", "dist", ".output"}:
            continue
        if allowed_names is not None and path.name not in allowed_names:
            continue
        target = dest / path.name
        if path.is_dir():
            shutil.copytree(path, target, dirs_exist_ok=True)
        else:
            _copy_file(path, target)


def _plan() -> dict:
    def block(block_id: str, reason: str, slots: dict, deps: list, bindings: list) -> dict:
        return {
            "id": block_id,
            "version": "1.0.0",
            "reason": reason,
            "slots": slots,
            "dependencies": deps,
            "protected_bindings": bindings,
        }

    pack = lambda slug: {"kind": "feature_pack", "id": slug, "min_version": None}
    prim = lambda slug: {"kind": "primitive", "id": slug, "min_version": None}
    app = lambda slug: {"kind": "app_block", "id": slug, "min_version": None}
    return {
        "schema_version": "1",
        "requested_capabilities": ["crm.customer", "billing.invoice", "reporting.dashboard"],
        "blocks": [
            block(
                "admin.shell",
                "Staff chrome for the accounting gold seed.",
                {"route": "/admin", "title": "Practice books"},
                [prim("auth.roles")],
                ["admin_auth", "admin_bff"],
            ),
            block(
                "admin.record_table",
                "Shared listing host for customers, invoices, and reports.",
                {"entity": "customer"},
                [prim("data.entity")],
                [],
            ),
            block(
                "admin.record_detail",
                "Shared detail host for customers, invoices, and reports.",
                {"entity": "customer"},
                [prim("data.entity")],
                [],
            ),
            block(
                "crm.customer",
                "Client records for the practice. Not a pipeline.",
                {"route": "/customers", "heading": "Customers"},
                [pack("customer-records"), prim("data.entity"), app("admin.shell"), app("admin.record_table"), app("admin.record_detail")],
                ["persistence", "allowed_fields"],
            ),
            block(
                "billing.invoice",
                "Invoice records. paid is status, not a payment.",
                {"route": "/invoices", "heading": "Invoices"},
                [pack("billing-management"), prim("data.entity"), app("admin.shell"), app("admin.record_table"), app("admin.record_detail")],
                ["persistence", "allowed_fields"],
            ),
            block(
                "reporting.dashboard",
                "Saved report definitions over Cloud records.",
                {"route": "/reports", "heading": "Reports"},
                [pack("reporting-dashboards"), prim("data.entity"), app("admin.shell"), app("admin.record_table"), app("admin.record_detail")],
                ["persistence", "allowed_fields"],
            ),
        ],
        "required_feature_packs": ["customer-records", "billing-management", "reporting-dashboards"],
        "required_primitives": ["data.entity", "auth.roles"],
        "configuration_requirements": [],
        "gaps": [
            {
                "type": "unsupported",
                "id": "full_accounting_gl",
                "severity": "optional",
                "affects": [],
                "message": "Not a general ledger. Do not invent double-entry or checkout.",
            }
        ],
        "runtime_dependencies": [],
        "starter_contracts": [
            "form.standard",
            "record.table",
            "record.create",
            "record.edit",
            "record.detail",
            "layout.admin",
            "layout.page_header",
            "state.empty",
        ],
        "adoption": "authoritative",
        "matched_recipe_id": None,
        "starter_id": "internal_operations",
        "feature_pack_ids": ["customer-records", "billing-management", "reporting-dashboards"],
        "forbidden_capabilities": ["payments.checkout"],
        "product_archetype": "internal_operations",
        "unsupported_assumptions": [
            "Not a general ledger",
            "Not Stripe checkout or Core workspace invoices",
        ],
    }


def _hosts() -> dict:
    src = json.loads((BOILER / "p2hs" / "app-block-hosts.json").read_text(encoding="utf-8"))
    keep = {
        "admin.shell",
        "admin.record_table",
        "admin.record_detail",
        "crm.customer",
        "billing.invoice",
        "reporting.dashboard",
        "files.attachments",
        "auth.sign_in",
    }
    return {
        "version": src.get("version", 1),
        "hosts": [row for row in src["hosts"] if row.get("block") in keep],
    }


def _contracts() -> dict:
    src = json.loads((BOILER / "p2hs" / "starter-contracts.json").read_text(encoding="utf-8"))
    keep = {
        "token.color",
        "token.spacing",
        "token.typography",
        "token.radius",
        "token.shadow",
        "ui.button",
        "ui.input",
        "ui.textarea",
        "form.field",
        "form.standard",
        "record.table",
        "record.detail",
        "record.create",
        "record.edit",
        "state.empty",
        "state.success",
        "layout.page",
        "layout.admin",
        "layout.page_header",
        "file.upload",
        "file.attachments",
        "adapter.auth",
        "adapter.records",
        "adapter.files",
    }
    return {
        "starter_contract_version": src.get("starter_contract_version", 1),
        "contracts": [row for row in src["contracts"] if row.get("id") in keep],
    }


def _pages() -> dict[str, str]:
    return {
        "app.vue": """<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
""",
        "layouts/default.vue": """<script setup>
const route = useRoute();
</script>

<template>
  <StarterAdminLayout title="Practice books">
    <template #nav>
      <NuxtLink to="/admin">Overview</NuxtLink>
      <NuxtLink to="/customers">Customers</NuxtLink>
      <NuxtLink to="/invoices">Invoices</NuxtLink>
      <NuxtLink to="/reports">Reports</NuxtLink>
    </template>
    <slot />
  </StarterAdminLayout>
</template>
""",
        "pages/index.vue": """<script setup>
await navigateTo("/admin");
</script>
""",
        "pages/admin/index.vue": """<template>
  <section data-app-block="admin.shell" data-route="/admin">
    <StarterPageHeader
      title="Practice books"
      description="Customers, invoice records, and saved reports. Not a general ledger. Data goes through /api."
    />
    <ul class="starter-stack">
      <li><NuxtLink to="/customers">Customers</NuxtLink></li>
      <li><NuxtLink to="/invoices">Invoices</NuxtLink></li>
      <li><NuxtLink to="/reports">Reports</NuxtLink></li>
    </ul>
  </section>
</template>
""",
        "pages/customers/index.vue": """<script setup>
import CustomerTable from "~/components/starter/blocks/CustomerTable.vue";
function openRow(row) {
  if (row?.id) navigateTo(`/customers/${row.id}`);
}
</script>
<template>
  <section data-app-block="crm.customer" data-route="/customers">
    <StarterPageHeader title="Customers" description="Standing client records. Status is active/inactive, not a pipeline.">
      <template #actions>
        <NuxtLink to="/customers/new">New customer</NuxtLink>
      </template>
    </StarterPageHeader>
    <CustomerTable @select="openRow" />
  </section>
</template>
""",
        "pages/customers/new.vue": """<script setup>
import CustomerCreate from "~/components/starter/blocks/CustomerCreate.vue";
function onCreated(record) {
  if (record?.id) navigateTo(`/customers/${record.id}`);
  else navigateTo("/customers");
}
</script>
<template>
  <section data-app-block="crm.customer" data-route="/customers/new">
    <StarterPageHeader title="New customer" />
    <CustomerCreate @created="onCreated" />
  </section>
</template>
""",
        "pages/customers/[id]/index.vue": """<script setup>
import CustomerDetail from "~/components/starter/blocks/CustomerDetail.vue";
const route = useRoute();
</script>
<template>
  <section data-app-block="crm.customer" data-route="/customers/:id">
    <StarterPageHeader title="Customer">
      <template #actions>
        <NuxtLink :to="`/customers/${route.params.id}/edit`">Edit</NuxtLink>
      </template>
    </StarterPageHeader>
    <CustomerDetail :record-id="String(route.params.id)" />
  </section>
</template>
""",
        "pages/customers/[id]/edit.vue": """<script setup>
import CustomerEdit from "~/components/starter/blocks/CustomerEdit.vue";
const route = useRoute();
function onUpdated() {
  navigateTo(`/customers/${route.params.id}`);
}
</script>
<template>
  <section data-app-block="crm.customer" data-route="/customers/:id/edit">
    <StarterPageHeader title="Edit customer" />
    <CustomerEdit :record-id="String(route.params.id)" @updated="onUpdated" />
  </section>
</template>
""",
        "pages/invoices/index.vue": """<script setup>
import InvoiceTable from "~/components/starter/blocks/InvoiceTable.vue";
function openRow(row) {
  if (row?.id) navigateTo(`/invoices/${row.id}`);
}
</script>
<template>
  <section data-app-block="billing.invoice" data-route="/invoices">
    <StarterPageHeader title="Invoices" description="Application invoice records. paid is status, not proof of a payment.">
      <template #actions>
        <NuxtLink to="/invoices/new">New invoice</NuxtLink>
      </template>
    </StarterPageHeader>
    <InvoiceTable @select="openRow" />
  </section>
</template>
""",
        "pages/invoices/new.vue": """<script setup>
import InvoiceCreate from "~/components/starter/blocks/InvoiceCreate.vue";
function onCreated(record) {
  if (record?.id) navigateTo(`/invoices/${record.id}`);
  else navigateTo("/invoices");
}
</script>
<template>
  <section data-app-block="billing.invoice" data-route="/invoices/new">
    <StarterPageHeader title="New invoice" />
    <InvoiceCreate @created="onCreated" />
  </section>
</template>
""",
        "pages/invoices/[id]/index.vue": """<script setup>
import InvoiceDetail from "~/components/starter/blocks/InvoiceDetail.vue";
const route = useRoute();
</script>
<template>
  <section data-app-block="billing.invoice" data-route="/invoices/:id">
    <StarterPageHeader title="Invoice">
      <template #actions>
        <NuxtLink :to="`/invoices/${route.params.id}/edit`">Edit</NuxtLink>
      </template>
    </StarterPageHeader>
    <InvoiceDetail :record-id="String(route.params.id)" />
  </section>
</template>
""",
        "pages/invoices/[id]/edit.vue": """<script setup>
import InvoiceEdit from "~/components/starter/blocks/InvoiceEdit.vue";
const route = useRoute();
function onUpdated() {
  navigateTo(`/invoices/${route.params.id}`);
}
</script>
<template>
  <section data-app-block="billing.invoice" data-route="/invoices/:id/edit">
    <StarterPageHeader title="Edit invoice" />
    <InvoiceEdit :record-id="String(route.params.id)" @updated="onUpdated" />
  </section>
</template>
""",
        "pages/reports/index.vue": """<script setup>
import ReportTable from "~/components/starter/blocks/ReportTable.vue";
function openRow(row) {
  if (row?.id) navigateTo(`/reports/${row.id}`);
}
</script>
<template>
  <section data-app-block="reporting.dashboard" data-route="/reports">
    <StarterPageHeader title="Reports" description="Saved report definitions. Aggregates run at view time on existing records.">
      <template #actions>
        <NuxtLink to="/reports/new">New report</NuxtLink>
      </template>
    </StarterPageHeader>
    <ReportTable @select="openRow" />
  </section>
</template>
""",
        "pages/reports/new.vue": """<script setup>
import ReportCreate from "~/components/starter/blocks/ReportCreate.vue";
function onCreated(record) {
  if (record?.id) navigateTo(`/reports/${record.id}`);
  else navigateTo("/reports");
}
</script>
<template>
  <section data-app-block="reporting.dashboard" data-route="/reports/new">
    <StarterPageHeader title="New report" />
    <ReportCreate @created="onCreated" />
  </section>
</template>
""",
        "pages/reports/[id]/index.vue": """<script setup>
import ReportDetail from "~/components/starter/blocks/ReportDetail.vue";
const route = useRoute();
</script>
<template>
  <section data-app-block="reporting.dashboard" data-route="/reports/:id">
    <StarterPageHeader title="Report">
      <template #actions>
        <NuxtLink :to="`/reports/${route.params.id}/edit`">Edit</NuxtLink>
      </template>
    </StarterPageHeader>
    <ReportDetail :record-id="String(route.params.id)" />
  </section>
</template>
""",
        "pages/reports/[id]/edit.vue": """<script setup>
import ReportEdit from "~/components/starter/blocks/ReportEdit.vue";
const route = useRoute();
function onUpdated() {
  navigateTo(`/reports/${route.params.id}`);
}
</script>
<template>
  <section data-app-block="reporting.dashboard" data-route="/reports/:id/edit">
    <StarterPageHeader title="Edit report" />
    <ReportEdit :record-id="String(route.params.id)" @updated="onUpdated" />
  </section>
</template>
""",
    }


def assemble(dest: Path) -> None:
    if not BOILER.is_dir():
        raise SystemExit(f"Sealed Nuxt boilerplate missing at {BOILER}")
    dest.mkdir(parents=True, exist_ok=True)

    _copy_file(BOILER / "package.json", dest / "package.json")
    _copy_file(BOILER / "nuxt.config.ts", dest / "nuxt.config.ts")
    if (BOILER / "assets").is_dir():
        shutil.copytree(BOILER / "assets", dest / "assets", dirs_exist_ok=True)
    _copy_dir_filtered(BOILER / "components" / "starter", dest / "components" / "starter", allowed_names=None)
    # Drop unused block hosts so this is a gold product, not the full overlay kit.
    blocks = dest / "components" / "starter" / "blocks"
    if blocks.is_dir():
        for path in list(blocks.iterdir()):
            if path.name not in _BLOCK_HOSTS:
                path.unlink()
    (dest / "lib" / "entity-schemas").mkdir(parents=True, exist_ok=True)
    _copy_file(BOILER / "lib" / "record-form.mjs", dest / "lib" / "record-form.mjs")
    _copy_file(BOILER / "lib" / "p2hs-app.js", dest / "lib" / "p2hs-app.js")
    for name in _SCHEMAS:
        _copy_file(BOILER / "lib" / "entity-schemas" / name, dest / "lib" / "entity-schemas" / name)
    _copy_file(BOILER / "composables" / "useP2hsApi.ts", dest / "composables" / "useP2hsApi.ts")
    _copy_file(BOILER / "composables" / "useP2hs.ts", dest / "composables" / "useP2hs.ts")

    _write(dest / ".gitignore", _GITIGNORE)
    _write(dest / ".env.example", _ENV_EXAMPLE)
    _write(dest / "README.md", _README)
    _write(dest / "p2hs" / "cloud-access.md", _CLOUD_ACCESS)
    _write(dest / "p2hs" / "app-blocks-plan.json", json.dumps(_plan(), indent=2) + "\n")
    _write(dest / "p2hs" / "app-block-hosts.json", json.dumps(_hosts(), indent=2) + "\n")
    _write(dest / "p2hs" / "starter-contracts.json", json.dumps(_contracts(), indent=2) + "\n")
    for relative, content in _pages().items():
        _write(dest / relative, content)

    from src.domain.app_blocks.schemas import AppBlockPlan
    from src.domain.codegen.port_adapter import install_catalog_adapters
    from src.domain.templates.service import build_template_deploy_workflow

    AppBlockPlan.model_validate_json((dest / "p2hs" / "app-blocks-plan.json").read_text(encoding="utf-8"))
    install_catalog_adapters(str(dest), "nuxt_app", {"legacy_contact_submit": False})
    _write(
        dest / ".github" / "workflows" / "deploy.yml",
        build_template_deploy_workflow(
            ref=REF,
            framework="nuxt_app",
            main_branch="main",
            dev_branch="development",
        ),
    )
    leaked = (
        "NUXT_PUBLIC_P2HS_GATEWAY_URL",
        "VITE_P2HS_GATEWAY_URL",
        "NUXT_PUBLIC_P2HS_CLOUD_PUBLIC_URL",
        "VITE_P2HS_CLOUD_PUBLIC_URL",
        "NUXT_PUBLIC_P2HS_CORE_PUBLIC_URL",
        "VITE_P2HS_CORE_PUBLIC_URL",
        "gatewayPublicUrl",
        "cloudgate.p2hs.com",
        "payments.checkout",
    )
    for path in dest.rglob("*"):
        if not path.is_file() or any(part in {".git", "node_modules"} for part in path.parts):
            continue
        if path.suffix.lower() in {".png", ".jpg", ".jpeg", ".webp", ".gz", ".woff", ".woff2"}:
            continue
        text = path.read_text(encoding="utf-8", errors="ignore")
        for marker in leaked:
            if marker not in text:
                continue
            if marker == "payments.checkout" and path.name == "app-blocks-plan.json":
                continue
            raise SystemExit(f"gold tree leaked {marker} in {path.relative_to(dest)}")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--dest", required=True)
    parser.add_argument("--replace", action="store_true", help="Delete existing files in dest except .git")
    args = parser.parse_args()
    dest = Path(args.dest).resolve()
    if args.replace and dest.is_dir():
        for child in dest.iterdir():
            if child.name == ".git":
                continue
            if child.is_dir():
                shutil.rmtree(child)
            else:
                child.unlink()
    assemble(dest)
    print(f"assembled gold accounting-nuxt -> {dest}")


if __name__ == "__main__":
    main()
