"""Phase 10G — materialize the 10F quote vertical as a generated product tree.

Codegen owns placement, copy, and host mounting. Persistence, query, record
read, workflow legality, auth/BFF, and events stay on platform contracts.
Does not add App Blocks, Feature Packs, catalog events, Cloud, or Dashboard.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from pathlib import Path
from uuid import uuid4

from src.domain.app_blocks.adoption import adoption_for, apply_adoption
from src.domain.app_blocks.catalogs import default_primitives
from src.domain.app_blocks.execution import assert_install_plan_installable
from src.domain.app_blocks.hosts import host_map_payload, merge_host_map
from src.domain.app_blocks.quote_vertical import (
    compile_quote_vertical_plan,
    generated_quote_routes,
    quote_entity_key,
    quote_transition_allowed,
)
from src.domain.app_blocks.schemas import AppBlockPlan
from src.domain.codegen.app_sdk import app_sdk_files, ensure_required_packs_from_plan
from src.domain.projects.models import PROJECT_SOURCE_KIND_GENERATED

RUNTIME_DEPENDENCY_STATUSES = (
    "ready",
    "configuration_required",
    "degraded",
    "disabled",
    "unsupported",
)

QUOTE_FORM_FIELDS = (
    {"key": "name", "type": "text", "label": "Name", "required": True},
    {"key": "email", "type": "email", "label": "Email", "required": True},
    {"key": "phone", "type": "phone", "label": "Phone", "required": False},
    {"key": "details", "type": "textarea", "label": "Project details", "required": True},
)

QUOTE_LIST_COLUMNS = (
    {"key": "name", "label": "Name"},
    {"key": "email", "label": "Email"},
    {"key": "status", "label": "Status"},
)

HOST_MOUNTS = {
    ("forms.quote_request", "public_form"): "components/starter/blocks/QuotePublicForm.vue",
    ("forms.quote_request", "admin_list"): "pages/admin/quotes/index.vue",
    ("forms.quote_request", "admin_detail"): "pages/admin/quotes/[id].vue",
    ("admin.record_table", "table"): "components/starter/RecordTable.vue",
    ("admin.record_detail", "detail"): "components/starter/RecordDetail.vue",
    ("admin.shell", "layout"): "layouts/admin.vue",
}

FORBIDDEN_GENERATED_MARKERS = (
    "new PrismaClient",
    "better-sqlite3",
    "from 'sqlite3'",
    'from "sqlite3"',
    "createClient(",
    "supabase",
    "new Resend",
    "from 'resend'",
    'from "resend"',
    "ALLOWED_TRANSITIONS",
    "validateWorkflowTransition(",
    "emitDomainEvent",
    "POST('/events')",
    'POST("/events")',
    "/api/events",
    "/api/quote",
    "/api/quotes",
    "forms.quote_request.submitted",
)

_APPROVED_CALLS = ("submitGeneratedForm", "listRecords", "getRecord", "patchRecord")


class WorkflowTransitionRejected(Exception):
    """Platform workflow choke point (Core graph / Cloud enforcement stand-in)."""

    def __init__(self, from_state: str, to_state: str):
        self.from_state = from_state
        self.to_state = to_state
        self.status_code = 409
        super().__init__(f"workflow.transition rejected: {from_state} → {to_state}")


def quote_plan_without_email() -> AppBlockPlan:
    primitives = frozenset(item for item in default_primitives() if item != "email.send")
    return apply_adoption(
        compile_quote_vertical_plan(primitives=primitives),
        adoption_for(source_kind=PROJECT_SOURCE_KIND_GENERATED, completed_codegen=False),
    )


def classify_runtime_dependencies(
    plan: AppBlockPlan, *, configured: frozenset[str] | set[str] | None = None
) -> dict[str, dict]:
    """Map optional runtime deps to explicit statuses, not a Boolean available flag."""
    configured = set(configured or ())
    rows: dict[str, dict] = {}
    by_cap = {item.capability: item for item in plan.configuration_requirements}
    for capability in plan.runtime_dependencies:
        requirement = by_cap.get(capability)
        label = "Email notifications" if capability == "email.send" else capability
        if capability in configured:
            status = "ready"
        elif requirement is not None and requirement.behavior == "disabled":
            status = "disabled"
        else:
            status = "configuration_required"
        if status not in RUNTIME_DEPENDENCY_STATUSES:
            raise ValueError(f"unknown runtime dependency status: {status}")
        rows[capability] = {
            "capability": capability,
            "status": status,
            "available": status == "ready",
            "label": label,
            "setup_required": status == "configuration_required",
            "providers": list(requirement.providers) if requirement else [],
        }
    return rows


def mount_quote_vertical_hosts(existing: dict | None = None) -> dict:
    catalog = host_map_payload()
    merged = merge_host_map(existing, catalog)
    hosts = []
    for item in merged.get("hosts") or []:
        row = dict(item)
        key = (str(row.get("block") or ""), str(row.get("slot") or ""))
        if key in HOST_MOUNTS:
            row["implementation"] = HOST_MOUNTS[key]
        hosts.append(row)
    return {**merged, "hosts": hosts}


def materialize_quote_vertical_product(
    root: str | Path, *, plan: AppBlockPlan | None = None
) -> AppBlockPlan:
    """Write a Nuxt-shaped generated app that mounts 10F hosts and platform calls."""
    root = Path(root)
    plan = plan or quote_plan_without_email()
    assert_install_plan_installable(plan)
    routes = generated_quote_routes(plan)
    quote = next(block for block in plan.blocks if block.id == "forms.quote_request")
    table = next(block for block in plan.blocks if block.id == "admin.record_table")
    heading = str(quote.slots.get("heading") or "Request a quote")
    submit_label = str(quote.slots.get("submit_label") or "Request quote")
    empty_state = str(table.slots.get("empty_state") or "No quotes yet")
    entity_key = quote_entity_key()
    deps = classify_runtime_dependencies(plan)

    files = dict(app_sdk_files("nuxt_app"))
    files.update(
        {
            "p2hs/app-blocks-plan.json": json.dumps(plan.model_dump(), indent=2) + "\n",
            "p2hs/app-block-hosts.json": json.dumps(mount_quote_vertical_hosts(), indent=2)
            + "\n",
            "p2hs/runtime-dependencies.json": json.dumps(deps, indent=2) + "\n",
            "lib/runtime-dependencies.js": _runtime_dependencies_module(deps),
            "components/starter/blocks/QuotePublicForm.vue": _quote_public_form(
                heading, submit_label, entity_key
            ),
            "components/starter/RecordTable.vue": _record_table(empty_state, entity_key),
            "components/starter/RecordDetail.vue": _record_detail(entity_key),
            "layouts/admin.vue": _admin_layout(),
            "pages/request-quote.vue": _public_page(),
            "pages/admin/quotes/index.vue": _list_page(),
            "pages/admin/quotes/[id].vue": _detail_page(),
            "pages/admin/settings.vue": _settings_page(),
        }
    )
    for rel, source in files.items():
        path = root / rel
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(source, encoding="utf-8", newline="\n")
    ensure_required_packs_from_plan(str(root))
    if routes["public"] != "/request-quote":
        raise ValueError("10G uses the 10F public route; do not invent a second public form")
    return plan


def inspect_generated_quote_product(root: str | Path) -> dict:
    root = Path(root)
    blob = _concat_sources(root)
    hosts = json.loads((root / "p2hs" / "app-block-hosts.json").read_text(encoding="utf-8"))
    deps = json.loads((root / "p2hs" / "runtime-dependencies.json").read_text(encoding="utf-8"))
    mounted = {
        f"{item['block']}#{item['slot']}": item.get("implementation")
        for item in hosts.get("hosts") or []
        if item.get("implementation")
    }
    return {
        "approved_calls": {name: name in blob for name in _APPROVED_CALLS},
        "forbidden_markers": [marker for marker in FORBIDDEN_GENERATED_MARKERS if marker in blob],
        "host_mounts": mounted,
        "runtime_dependencies": deps,
        "routes": {
            "public": (root / "pages" / "request-quote.vue").is_file(),
            "admin_list": (root / "pages" / "admin" / "quotes" / "index.vue").is_file(),
            "admin_detail": (root / "pages" / "admin" / "quotes" / "[id].vue").is_file(),
        },
        "email_status": (deps.get("email.send") or {}).get("status"),
    }


def assert_generated_quote_artifact(root: str | Path) -> dict:
    report = inspect_generated_quote_product(root)
    missing = [name for name, present in report["approved_calls"].items() if not present]
    if missing:
        raise ValueError(f"generated product missing platform calls: {missing}")
    if report["forbidden_markers"]:
        raise ValueError(f"generated product contains forbidden markers: {report['forbidden_markers']}")
    for key, rel in HOST_MOUNTS.items():
        mounted = report["host_mounts"].get(f"{key[0]}#{key[1]}")
        if mounted != rel:
            raise ValueError(f"host slot {key} not mounted at {rel}: {mounted}")
    if not all(report["routes"].values()):
        raise ValueError("generated quote routes are missing")
    if report["email_status"] != "configuration_required":
        raise ValueError("email must remain configuration_required when unconfigured")
    return report


@dataclass
class QuoteRecord:
    id: str
    data: dict


@dataclass
class QuoteVerticalRuntime:
    """In-process platform kernel the generated pages would call through BFF."""

    entity_key: str = field(default_factory=quote_entity_key)
    records: dict[str, QuoteRecord] = field(default_factory=dict)
    domain_events: list[str] = field(default_factory=list)

    def submit_generated_form(self, *, form_key: str, values: dict) -> QuoteRecord:
        if form_key != self.entity_key:
            raise ValueError("public submit must use formKey quote_request")
        record = QuoteRecord(
            id=str(uuid4()),
            data={**values, "status": "new"},
        )
        self.records[record.id] = record
        return record

    def list_records(self, entity_key: str) -> list[dict]:
        if entity_key != self.entity_key:
            raise ValueError("admin list query is owned by admin.record_table for quote_request")
        return [{"id": item.id, **item.data} for item in self.records.values()]

    def get_record(self, entity_key: str, record_id: str) -> dict:
        if entity_key != self.entity_key:
            raise ValueError("record_read is owned by admin.record_detail for quote_request")
        record = self.records[record_id]
        return {"id": record.id, **record.data}

    def patch_record(self, entity_key: str, record_id: str, data: dict) -> dict:
        if entity_key != self.entity_key:
            raise ValueError("mutations stay on the quote_request entity")
        record = self.records[record_id]
        if "status" in data:
            current = str(record.data.get("status") or "")
            nxt = str(data["status"])
            if not quote_transition_allowed(current, nxt):
                raise WorkflowTransitionRejected(current, nxt)
            record.data["status"] = nxt
        for key, value in data.items():
            if key != "status":
                record.data[key] = value
        return {"id": record.id, **record.data}

    def emit_quote_submitted_event(self) -> None:
        raise RuntimeError("forms.quote_request.submitted is composition-only")


def _concat_sources(root: Path) -> str:
    parts: list[str] = []
    for path in sorted(root.rglob("*")):
        if not path.is_file():
            continue
        if path.suffix.lower() not in {".vue", ".js", ".ts", ".json", ".mjs"}:
            continue
        if "node_modules" in path.parts or ".git" in path.parts:
            continue
        parts.append(path.read_text(encoding="utf-8"))
    return "\n".join(parts)


def _runtime_dependencies_module(deps: dict) -> str:
    return (
        "// Generated runtime dependency snapshot. Do not invent a private provider client.\n"
        "export const runtimeDependencies = "
        + json.dumps(deps, indent=2)
        + ";\n"
        "export const RUNTIME_DEPENDENCY_STATUSES = "
        + json.dumps(list(RUNTIME_DEPENDENCY_STATUSES))
        + ";\n"
    )


def _quote_public_form(heading: str, submit_label: str, entity_key: str) -> str:
    field_rows = "\n".join(
        f'      <label>{item["label"]}<input name="{item["key"]}" v-model="values.{item["key"]}" /></label>'
        for item in QUOTE_FORM_FIELDS
    )
    return f"""<script setup>
import {{ submitGeneratedForm }} from '~/lib/p2hs-app.js'

const values = reactive({{
  name: '',
  email: '',
  phone: '',
  details: '',
}})
const submitted = ref(false)

async function onSubmit() {{
  await submitGeneratedForm({{ formKey: '{entity_key}', values }})
  submitted.value = true
}}
</script>

<template>
  <form class="quote-request-form" @submit.prevent="onSubmit">
    <h1>{heading}</h1>
    <p>Tell us about the roofing work you need. We will follow up with a quote.</p>
{field_rows}
    <button type="submit">{submit_label}</button>
    <p v-if="submitted">Thanks — we received your quote request.</p>
  </form>
</template>
"""


def _record_table(empty_state: str, entity_key: str) -> str:
    columns = json.dumps([item["label"] for item in QUOTE_LIST_COLUMNS])
    return f"""<script setup>
import {{ listRecords }} from '~/lib/p2hs-app.js'

const columns = {columns}
const rows = ref([])
const emptyState = {json.dumps(empty_state)}

onMounted(async () => {{
  rows.value = await listRecords('{entity_key}')
}})
</script>

<template>
  <section>
    <h1>Quotes</h1>
    <p v-if="!rows.length">{{{{ emptyState }}}}</p>
    <table v-else>
      <thead>
        <tr><th v-for="column in columns" :key="column">{{{{ column }}}}</th></tr>
      </thead>
      <tbody>
        <tr v-for="row in rows" :key="row.id">
          <td>{{{{ row.name }}}}</td>
          <td>{{{{ row.email }}}}</td>
          <td>{{{{ row.status }}}}</td>
        </tr>
      </tbody>
    </table>
  </section>
</template>
"""


def _record_detail(entity_key: str) -> str:
    return f"""<script setup>
import {{ getRecord, patchRecord }} from '~/lib/p2hs-app.js'

const route = useRoute()
const record = ref(null)
const error = ref('')

onMounted(async () => {{
  record.value = await getRecord('{entity_key}', route.params.id)
}})

async function setStatus(next) {{
  error.value = ''
  try {{
    record.value = await patchRecord('{entity_key}', route.params.id, {{ status: next }})
  }} catch (err) {{
    error.value = err?.status === 409 ? 'That status change is not allowed.' : 'Update failed.'
  }}
}}
</script>

<template>
  <section v-if="record">
    <h1>Quote {{{{ record.id }}}}</h1>
    <p>{{{{ record.name }}}} · {{{{ record.email }}}}</p>
    <p>Status: {{{{ record.status }}}}</p>
    <button type="button" @click="setStatus('reviewing')">Mark reviewing</button>
    <button type="button" @click="setStatus('quoted')">Mark quoted</button>
    <button type="button" @click="setStatus('won')">Mark won</button>
    <p v-if="error">{{{{ error }}}}</p>
  </section>
</template>
"""


def _admin_layout() -> str:
    return """<script setup>
import { me } from '~/lib/p2hs-app.js'

onMounted(async () => {
  await me()
})
</script>

<template>
  <div class="admin-shell">
    <nav>
      <NuxtLink to="/admin/quotes">Quotes</NuxtLink>
      <NuxtLink to="/admin/settings">Settings</NuxtLink>
    </nav>
    <slot />
  </div>
</template>
"""


def _public_page() -> str:
    return """<script setup>
import QuotePublicForm from '~/components/starter/blocks/QuotePublicForm.vue'
</script>

<template>
  <QuotePublicForm />
</template>
"""


def _list_page() -> str:
    return """<script setup>
import RecordTable from '~/components/starter/RecordTable.vue'

definePageMeta({ layout: 'admin' })
</script>

<template>
  <RecordTable />
</template>
"""


def _detail_page() -> str:
    return """<script setup>
import RecordDetail from '~/components/starter/RecordDetail.vue'

definePageMeta({ layout: 'admin' })
</script>

<template>
  <RecordDetail />
</template>
"""


def _settings_page() -> str:
    return """<script setup>
definePageMeta({ layout: 'admin' })
import { runtimeDependencies, RUNTIME_DEPENDENCY_STATUSES } from '~/lib/runtime-dependencies.js'

const email = runtimeDependencies['email.send']
</script>

<template>
  <section>
    <h1>Settings</h1>
    <div v-if="email && email.status === 'configuration_required'">
      <h2>{{ email.label }}</h2>
      <p>Setup required</p>
    </div>
    <p>Known states: {{ RUNTIME_DEPENDENCY_STATUSES.join(', ') }}</p>
  </section>
</template>
"""

