"""Worker task: rebuild and push the Shopify App CF Worker when functions change.

Regenerates:
  - src/registry.ts    (function imports + FUNCTIONS array)
  - src/functions/*.ts (one file per function with AI-generated code)
  - wrangler.toml      (cron trigger schedules)

Then commits and pushes to GitHub main branch. GitHub Actions runs
`wrangler deploy` automatically after the push.
"""
from __future__ import annotations

import os
import textwrap
from uuid import UUID

import structlog

logger = structlog.get_logger()


def _slug_to_identifier(slug: str) -> str:
    """Convert kebab-case slug to camelCase JS identifier for imports."""
    parts = slug.split("-")
    return parts[0] + "".join(p.title() for p in parts[1:])


def _generate_function_file(slug: str, code_ts: str) -> str:
    """Wrap AI-generated code in the standard function module shape."""
    return (
        f"// src/functions/{slug}.ts\n"
        "// This file is managed by P2HS. Re-run AI generation to update it.\n"
        "import type { Env } from '../registry.js';\n\n"
        f"{code_ts.strip()}\n"
    )


def _generate_registry_ts(functions: list[dict]) -> str:
    """Regenerate src/registry.ts from the current function list."""
    active = [f for f in functions if f["code_ts"] and f["status"] != "disabled"]

    import_lines = [
        f"import {{ run as {_slug_to_identifier(f['slug'])}Run }} from './functions/{f['slug']}.js';"
        for f in active
    ]

    fn_entries = []
    for f in active:
        ident = _slug_to_identifier(f["slug"])
        entry_lines = [
            "  {",
            f"    name: {_json_str(f['name'])},",
            f"    slug: {_json_str(f['slug'])},",
        ]
        if f.get("description"):
            entry_lines.append(f"    description: {_json_str(f['description'])},")
        entry_lines.append(f"    triggerType: {_json_str(f['trigger_type'])},")
        if f.get("cron_schedule"):
            entry_lines.append(f"    cronSchedule: {_json_str(f['cron_schedule'])},")
        if f.get("webhook_topic"):
            entry_lines.append(f"    webhookTopic: {_json_str(f['webhook_topic'])},")
        entry_lines.append(f"    status: {_json_str(f['status'])},")
        entry_lines.append(f"    run: {ident}Run,")
        entry_lines.append("  },")
        fn_entries.append("\n".join(entry_lines))

    imports_block = "\n".join(import_lines) if import_lines else "// No active functions yet"
    entries_block = "\n".join(fn_entries) if fn_entries else "  // No active functions yet"

    return (
        "/**\n"
        " * Function registry — managed by P2HS. Do not edit manually.\n"
        " * Add or update functions via the P2HS Dashboard.\n"
        " */\n\n"
        "export interface FunctionMeta {\n"
        "  name: string;\n"
        "  slug: string;\n"
        "  description?: string;\n"
        "  triggerType: 'cron' | 'webhook' | 'manual';\n"
        "  cronSchedule?: string;\n"
        "  webhookTopic?: string;\n"
        "  status: 'active' | 'draft' | 'disabled';\n"
        "  run: (env: Env, ctx: ExecutionContext, payload?: unknown) => Promise<void>;\n"
        "}\n\n"
        "export interface Env {\n"
        "  SHOPIFY_SHOP_DOMAIN: string;\n"
        "  SHOPIFY_ACCESS_TOKEN: string;\n"
        "  P2HS_WEBHOOK_SECRET: string;\n"
        "  P2HS_PREVIEW_JWT_SECRET: string;\n"
        "}\n\n"
        f"{imports_block}\n\n"
        "export const FUNCTIONS: FunctionMeta[] = [\n"
        f"{entries_block}\n"
        "];\n"
    )


def _generate_wrangler_toml(
    worker_name: str,
    functions: list[dict],
    resources: dict | None = None,
    cloud_url: str = "",
    cloud_client_id: str = "",
) -> str:
    """Regenerate wrangler.toml with current cron schedules and CF resource bindings."""
    cron_schedules = sorted({
        f["cron_schedule"]
        for f in functions
        if f["trigger_type"] == "cron" and f["cron_schedule"] and f["status"] == "active"
    })

    lines = [
        f'name = "{worker_name}"',
        'main = "src/index.ts"',
        'compatibility_date = "2024-01-01"',
        'compatibility_flags = ["nodejs_compat"]',
        "",
        "# Worker Secrets (set via wrangler or P2HS dashboard):",
        "#   SHOPIFY_ACCESS_TOKEN, P2HS_WEBHOOK_SECRET, P2HS_PREVIEW_JWT_SECRET",
        "# Non-sensitive vars (injected by P2HS below): P2HS_CLOUD_URL, P2HS_CLIENT_ID",
        "",
    ]

    if cron_schedules:
        quoted = ", ".join(f'"{c}"' for c in cron_schedules)
        lines += [
            "[triggers]",
            f"crons = [{quoted}]",
            "",
        ]
    else:
        lines += [
            "# No scheduled functions yet.",
            "# [triggers]",
            "# crons = []",
            "",
        ]

    if cloud_url or cloud_client_id:
        lines += ["[vars]"]
        if cloud_url:
            lines.append(f'P2HS_CLOUD_URL = "{cloud_url}"')
        if cloud_client_id:
            lines.append(f'P2HS_CLIENT_ID = "{cloud_client_id}"')
        lines.append("")

    if resources:
        if resources.get("d1_database_id") and resources.get("d1_database_name"):
            lines += [
                "[[d1_databases]]",
                'binding = "DB"',
                f'database_name = "{resources["d1_database_name"]}"',
                f'database_id = "{resources["d1_database_id"]}"',
                "",
            ]

        if resources.get("queue_name"):
            lines += [
                "[[queues.producers]]",
                'binding = "JOB_QUEUE"',
                f'queue = "{resources["queue_name"]}"',
                "",
                "[[queues.consumers]]",
                f'queue = "{resources["queue_name"]}"',
                "max_batch_size = 10",
                "max_batch_timeout = 30",
                "",
            ]

        if resources.get("r2_bucket_name"):
            lines += [
                "[[r2_buckets]]",
                'binding = "FILES"',
                f'bucket_name = "{resources["r2_bucket_name"]}"',
                "",
            ]

        if resources.get("kv_namespace_id"):
            lines += [
                "[[kv_namespaces]]",
                'binding = "CACHE"',
                f'id = "{resources["kv_namespace_id"]}"',
                "",
            ]

    return "\n".join(lines)


def _json_str(s: str) -> str:
    return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'


async def rebuild_shopify_app_worker(ctx: dict, project_id: str) -> dict:
    """Push regenerated registry.ts + function files + wrangler.toml to GitHub.

    GitHub Actions runs `wrangler deploy` automatically on main-branch push.
    """
    from src.core.database import async_session_factory
    from src.config import get_settings
    from src.domain.projects.repository import ProjectRepository, ProjectRepoRepository
    from src.domain.shopify_apps.repository import (
        ShopifyAppConfigRepository,
        ShopifyAppFunctionRepository,
        ShopifyAppResourcesRepository,
    )
    from src.integrations.github_client import GitHubAppClient
    from src.integrations.github_installation import resolve_installation_id_for_workspace

    settings = get_settings()
    db_factory = ctx.get("db_factory") or async_session_factory
    pid = UUID(project_id)

    async with db_factory() as db:
        project = await ProjectRepository(db).get_by_id(pid)
        if not project or project.type != "shopify_app":
            logger.warning("shopify_app_rebuild_skipped", project_id=project_id, reason="not_shopify_app")
            return {"status": "skipped"}

        repo_row = await ProjectRepoRepository(db).get_by_project_id(pid)
        if not repo_row:
            logger.warning("shopify_app_rebuild_skipped", project_id=project_id, reason="no_repo")
            return {"status": "skipped"}

        cfg = await ShopifyAppConfigRepository(db).get(pid)
        functions_rows = await ShopifyAppFunctionRepository(db).list_for_project(pid)
        res_row = await ShopifyAppResourcesRepository(db).get(pid)
        workspace_id = project.workspace_id
        project_ref = project.project_ref or ""
        repo_full = repo_row.repo_full_name
        default_branch = repo_row.default_branch or "main"

    functions = [
        {
            "name": fn.name,
            "slug": fn.slug,
            "description": fn.description,
            "trigger_type": fn.trigger_type,
            "cron_schedule": fn.cron_schedule,
            "webhook_topic": fn.webhook_topic,
            "code_ts": fn.code_ts,
            "status": fn.status,
        }
        for fn in functions_rows
    ]

    from src.domain.hosting.deployment_targets import worker_script_name, generate_worker_deploy_workflow
    from src.config import get_settings
    settings = get_settings()

    w_name = (cfg.worker_name if cfg else None) or worker_script_name(project_ref)

    resources = None
    cloud_url = ""
    cloud_client_id = ""
    if res_row and res_row.status == "ready":
        resources = {
            "d1_database_id": res_row.d1_database_id,
            "d1_database_name": res_row.d1_database_name,
            "queue_name": res_row.queue_name,
            "r2_bucket_name": res_row.r2_bucket_name,
            "kv_namespace_id": res_row.kv_namespace_id,
        }
        cloud_client_id = res_row.cloud_client_id or ""
        cloud_url = (settings.cloud_control_api_url or "").rstrip("/")

    # Build the commit file tree
    commit_files: dict[str, str] = {}

    # wrangler.toml
    commit_files["wrangler.toml"] = _generate_wrangler_toml(
        w_name, functions, resources, cloud_url=cloud_url, cloud_client_id=cloud_client_id
    )

    # deploy.yml — regenerate so any workflow changes (e.g. new secrets) land in the repo
    commit_files[".github/workflows/deploy.yml"] = generate_worker_deploy_workflow(project_ref)

    # src/registry.ts
    commit_files["src/registry.ts"] = _generate_registry_ts(functions)

    # Individual function files (only active ones with code)
    for fn in functions:
        if fn["code_ts"] and fn["status"] != "disabled":
            commit_files[f"src/functions/{fn['slug']}.ts"] = _generate_function_file(
                fn["slug"], fn["code_ts"]
            )

    installation_id = await resolve_installation_id_for_workspace(db_factory, workspace_id)
    github = GitHubAppClient(installation_id=installation_id)

    try:
        sha = await github.create_commit(
            repo_full,
            default_branch,
            commit_files,
            "chore(p2hs): update shopify app functions",
        )
        logger.info(
            "shopify_app_worker_rebuilt",
            project_id=project_id,
            repo=repo_full,
            sha=sha,
            functions_count=len(functions),
        )
    except Exception as exc:
        logger.error(
            "shopify_app_worker_rebuild_failed",
            project_id=project_id,
            repo=repo_full,
            error=str(exc),
        )
        raise

    # Try to set worker_url if not already known; re-sync Cloud so the URL lands in allowed_origins
    if cfg and not cfg.worker_url:
        await _try_set_worker_url(db_factory, pid, w_name, workspace_id, settings)
        async with db_factory() as db:
            from src.domain.shopify_apps.repository import ShopifyAppConfigRepository as _CfgRepo
            updated_cfg = await _CfgRepo(db).get(pid)
        if updated_cfg and updated_cfg.worker_url:
            try:
                from src.workers.enqueue import enqueue_cloud_sync_project
                await enqueue_cloud_sync_project(str(pid))
            except Exception:
                pass

    return {"status": "ok", "sha": sha, "functions": len(functions)}


async def _try_set_worker_url(db_factory, project_id: UUID, worker_name: str, workspace_id: UUID, settings) -> None:
    """Best-effort: resolve workers.dev subdomain from CF account and store worker_url."""
    try:
        cf_token = (settings.cloudflare_api_token or "").strip()
        cf_account = (settings.cloudflare_account_id or "").strip()

        async with db_factory() as db:
            from src.domain.cloud_infra.repository import (
                CFAccountRepository,
                CloudInstanceRepository,
                WorkspaceCloudAssignmentRepository,
            )
            from src.domain.cloud_infra.service import CloudInfraService
            from src.core.exceptions import NotFoundError

            try:
                svc = CloudInfraService(
                    cf_account_repo=CFAccountRepository(db),
                    instance_repo=CloudInstanceRepository(db),
                    assignment_repo=WorkspaceCloudAssignmentRepository(db),
                )
                creds = await svc.get_credentials_for_workspace(workspace_id)
                cf_token = creds.api_token
                cf_account = creds.account_id
            except (NotFoundError, ValueError):
                pass

        if not cf_token or not cf_account:
            return

        import httpx
        async with httpx.AsyncClient(timeout=10) as client:
            r = await client.get(
                f"https://api.cloudflare.com/client/v4/accounts/{cf_account}/workers/subdomain",
                headers={"Authorization": f"Bearer {cf_token}"},
            )
            if r.status_code == 200:
                subdomain = r.json().get("result", {}).get("subdomain", "")
                if subdomain:
                    worker_url = f"https://{worker_name}.{subdomain}.workers.dev"
                    async with db_factory() as db:
                        from src.domain.shopify_apps.repository import ShopifyAppConfigRepository
                        await ShopifyAppConfigRepository(db).upsert(project_id, worker_url=worker_url)
                        await db.commit()
                    logger.info("shopify_app_worker_url_set", project_id=str(project_id), worker_url=worker_url)
    except Exception as exc:
        logger.debug("shopify_app_worker_url_resolve_failed", project_id=str(project_id), error=str(exc))
