"""LLM-backed proposal generator for Autopilot Architect."""

from __future__ import annotations

import hashlib
import json
import os
import re
import shutil
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from g_gremlin import sfdc_metadata_pack
from g_gremlin import hubspot_pack

from .models import BlueprintDefinition, BlueprintSafety
from .plays import render_template


EMAIL_PATTERN = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+")
SFID_PATTERN = re.compile(r"\b[0-9A-Za-z]{15}(?:[0-9A-Za-z]{3})?\b")
USERNAME_PATTERN = re.compile(r"(?i)(username\s*[:=]\s*)([^\s,@<]+)")


def generate_proposal(
    blueprint: BlueprintDefinition,
    discovery_json: dict[str, Any],
    params: dict[str, Any],
    *,
    variant: str = "salesforce",
    provider: str | None = None,
    model: str | None = None,
    api_key: str | None = None,
) -> dict[str, Any]:
    prompt = _build_prompt(blueprint, discovery_json, params, variant=variant)
    provider, model, api_key = _resolve_llm_config(provider, model, api_key)
    response_text = _call_llm(prompt, provider, model, api_key)
    proposal = _extract_json(response_text)

    safety = blueprint.safety or BlueprintSafety()
    violations = _validate_proposal(proposal, safety, variant=variant)
    if violations:
        raise ValueError("Proposal violates safety limits: " + "; ".join(violations))

    return _ensure_proposal_metadata(proposal, blueprint, discovery_json, variant=variant)


def generate_skeleton(
    blueprint: BlueprintDefinition,
    discovery_json: dict[str, Any],
    params: dict[str, Any],
    *,
    variant: str = "salesforce",
) -> dict[str, Any]:
    base = {
        "summary": {
            "title": f"{blueprint.name} proposal (skeleton)",
            "goals": [],
            "risks": [],
            "notes": "Fill in proposal details manually.",
        },
        "metadata_pack": {
            "custom_fields": [],
            "validation_rules": [],
            "permission_sets": [],
        },
        "hubspot_pack": {
            "properties": [],
            "property_groups": [],
            "pipelines": [],
            "workflow_templates": [],
        },
        "matching_config": {},
        "assumptions": [],
        "discovery_snapshot": discovery_json,
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "parameters": params,
    }
    if variant == "hubspot":
        base.pop("metadata_pack", None)
    else:
        base.pop("hubspot_pack", None)
    return _ensure_proposal_metadata(base, blueprint, discovery_json, variant=variant)


def _build_prompt(
    blueprint: BlueprintDefinition,
    discovery_json: dict[str, Any],
    params: dict[str, Any],
    *,
    variant: str,
) -> str:
    prompt_template = _find_prompt_template(blueprint)
    safety = blueprint.safety.model_dump() if blueprint.safety else {}
    crm_context = (
        "Use Salesforce API names (__c suffix). Output metadata_pack section."
        if variant == "salesforce"
        else "Use HubSpot internal names (lowercase). Output hubspot_pack section."
    )
    payload = {
        "discovery_json": json.dumps(discovery_json, indent=2, ensure_ascii=True),
        "parameters_json": json.dumps(params, indent=2, ensure_ascii=True),
        "safety_json": json.dumps(safety, indent=2, ensure_ascii=True),
        "crm_variant": variant,
        "crm_context": crm_context,
    }
    return render_template(prompt_template, payload)


def _ensure_proposal_metadata(
    proposal: dict[str, Any],
    blueprint: BlueprintDefinition,
    discovery_json: dict[str, Any],
    *,
    variant: str,
) -> dict[str, Any]:
    proposal.setdefault("proposal_version", "1")
    proposal.setdefault("crm_variant", variant)
    proposal.setdefault("blueprint_name", blueprint.name)
    proposal.setdefault("generated_from_discovery_hash", _hash_payload(discovery_json))
    return proposal


def _find_prompt_template(blueprint: BlueprintDefinition) -> str:
    for phase in blueprint.phases.values():
        if phase.prompt_template:
            path = Path(phase.prompt_template)
            if path.is_absolute():
                return path.read_text(encoding="utf-8")
            from g_gremlin.config import get_config_dir
            base = get_config_dir() / "blueprints"
            candidate = base / phase.prompt_template
            if candidate.exists():
                return candidate.read_text(encoding="utf-8")
    raise ValueError("Prompt template not found for blueprint.")


def _resolve_llm_config(
    provider: str | None,
    model: str | None,
    api_key: str | None,
) -> tuple[str, str, str]:
    resolved_provider = provider
    if not resolved_provider:
        if os.getenv("ANTHROPIC_API_KEY"):
            resolved_provider = "anthropic"
        elif os.getenv("OPENAI_API_KEY"):
            resolved_provider = "openai"
        else:
            resolved_provider = "anthropic"

    resolved_api_key = api_key
    if not resolved_api_key:
        if resolved_provider == "openai":
            resolved_api_key = os.getenv("OPENAI_API_KEY", "")
        else:
            resolved_api_key = os.getenv("ANTHROPIC_API_KEY", "")

    resolved_model = model
    if not resolved_model:
        resolved_model = "gpt-4o" if resolved_provider == "openai" else "claude-sonnet-4-20250514"

    if not resolved_api_key:
        raise ValueError("LLM API key missing. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.")

    return resolved_provider, resolved_model, resolved_api_key


def _call_llm(prompt: str, provider: str, model: str, api_key: str) -> str:
    if provider == "openai":
        from openai import OpenAI

        client = OpenAI(api_key=api_key)
        try:
            response = client.chat.completions.create(
                model=model,
                messages=[{"role": "user", "content": prompt}],
                response_format={"type": "json_object"},
            )
        except TypeError:
            response = client.chat.completions.create(
                model=model,
                messages=[{"role": "user", "content": prompt}],
            )
        return response.choices[0].message.content or ""

    from anthropic import Anthropic

    client = Anthropic(api_key=api_key)
    response = client.messages.create(
        model=model,
        max_tokens=4096,
        messages=[{"role": "user", "content": prompt}],
    )
    content = []
    for block in response.content:
        if getattr(block, "type", "") == "text":
            content.append(block.text)
    return "\n".join(content)


def _extract_json(text: str) -> dict[str, Any]:
    if not text:
        raise ValueError("LLM response was empty.")
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass

    fenced = re.search(r"```json\s*(\{.*?\})\s*```", text, flags=re.DOTALL)
    if fenced:
        return json.loads(fenced.group(1))

    start = text.find("{")
    end = text.rfind("}")
    if start >= 0 and end > start:
        return json.loads(text[start : end + 1])
    raise ValueError("Failed to parse JSON from LLM response.")


def _validate_proposal(
    proposal: dict[str, Any],
    safety: BlueprintSafety,
    *,
    variant: str,
) -> list[str]:
    violations: list[str] = []
    if variant == "hubspot":
        pack = proposal.get("hubspot_pack") or {}
        properties = pack.get("properties") or []
        workflows = pack.get("workflow_templates") or []
        if safety.max_custom_properties is not None and len(properties) > safety.max_custom_properties:
            violations.append(
                f"properties exceeds max_custom_properties ({len(properties)} > {safety.max_custom_properties})"
            )
        if safety.max_workflows is not None and len(workflows) > safety.max_workflows:
            violations.append(
                f"workflow_templates exceeds max_workflows ({len(workflows)} > {safety.max_workflows})"
            )
    else:
        metadata = proposal.get("metadata_pack") or {}
        custom_fields = metadata.get("custom_fields") or []
        validation_rules = metadata.get("validation_rules") or []

        if safety.max_custom_fields is not None and len(custom_fields) > safety.max_custom_fields:
            violations.append(
                f"custom_fields exceeds max_custom_fields ({len(custom_fields)} > {safety.max_custom_fields})"
            )
        if safety.max_validation_rules is not None and len(validation_rules) > safety.max_validation_rules:
            violations.append(
                f"validation_rules exceeds max_validation_rules ({len(validation_rules)} > {safety.max_validation_rules})"
            )

    return violations


def generate_metadata_pack(
    proposal: dict[str, Any],
    project_dir: Path,
    *,
    variant: str = "salesforce",
) -> Path:
    if variant == "hubspot":
        return _generate_hubspot_pack(proposal, project_dir)
    return _generate_sfdc_metadata_pack(proposal, project_dir)


def _generate_sfdc_metadata_pack(proposal: dict[str, Any], project_dir: Path) -> Path:
    pack_dir = project_dir / "proposal" / "metadata_pack"
    _ensure_safe_pack_dir(pack_dir, project_dir)
    if pack_dir.exists():
        shutil.rmtree(pack_dir)
    sfdc_metadata_pack.init_pack(pack_dir)

    metadata = proposal.get("metadata_pack") or {}
    _write_custom_fields(pack_dir, metadata.get("custom_fields") or [])
    _write_validation_rules(pack_dir, metadata.get("validation_rules") or [])
    _write_permission_sets(pack_dir, metadata.get("permission_sets") or [])
    return pack_dir


def _generate_hubspot_pack(proposal: dict[str, Any], project_dir: Path) -> Path:
    pack_dir = project_dir / "proposal" / "hubspot_pack"
    _ensure_safe_pack_dir(pack_dir, project_dir)
    if pack_dir.exists():
        shutil.rmtree(pack_dir)
    hubspot_pack.init_pack(pack_dir)

    pack = proposal.get("hubspot_pack") or {}
    properties = pack.get("properties") or []
    property_groups = pack.get("property_groups") or []
    pipelines = pack.get("pipelines") or []
    workflow_templates = pack.get("workflow_templates") or []

    objects: dict[str, dict[str, Any]] = {}
    for group in property_groups:
        if not isinstance(group, dict):
            continue
        obj = group.get("object") or group.get("object_type") or "contacts"
        entry = objects.setdefault(str(obj).lower(), {"groups": [], "properties": []})
        entry["groups"].append(group)
    for prop in properties:
        if not isinstance(prop, dict):
            continue
        obj = prop.get("object") or prop.get("object_type") or "contacts"
        entry = objects.setdefault(str(obj).lower(), {"groups": [], "properties": []})
        entry["properties"].append(prop)

    if objects:
        properties_path = pack_dir / "properties.yaml"
        properties_payload = {"version": 1, "objects": objects}
        properties_path.write_text(json.dumps(properties_payload, indent=2, ensure_ascii=True), encoding="utf-8")

    pipelines_dir = pack_dir / "pipelines"
    pipelines_dir.mkdir(parents=True, exist_ok=True)
    for idx, pipeline in enumerate(pipelines, start=1):
        if not isinstance(pipeline, dict):
            continue
        name = pipeline.get("name") or pipeline.get("pipeline_id") or pipeline.get("id") or f"pipeline_{idx}"
        safe_name = _safe_api_name(str(name), append_custom_suffix=False)
        (pipelines_dir / f"{safe_name}.json").write_text(
            json.dumps(pipeline, indent=2, ensure_ascii=True),
            encoding="utf-8",
        )

    workflows_dir = pack_dir / "workflows"
    workflows_dir.mkdir(parents=True, exist_ok=True)
    for idx, flow in enumerate(workflow_templates, start=1):
        if not isinstance(flow, dict):
            continue
        name = flow.get("name") or flow.get("id") or flow.get("flowId") or f"workflow_{idx}"
        safe_name = _safe_api_name(str(name), append_custom_suffix=False)
        (workflows_dir / f"{safe_name}.json").write_text(
            json.dumps(flow, indent=2, ensure_ascii=True),
            encoding="utf-8",
        )

    return pack_dir


def generate_proposal_markdown(proposal: dict[str, Any]) -> str:
    summary = proposal.get("summary") or {}
    metadata = proposal.get("metadata_pack") or {}
    hubspot_pack_data = proposal.get("hubspot_pack") or {}
    lines = [
        f"# {summary.get('title', 'Architect Proposal')}",
        "",
        "## Goals",
    ]
    for goal in summary.get("goals") or []:
        lines.append(f"- {goal}")
    lines.append("")
    lines.append("## Risks")
    for risk in summary.get("risks") or []:
        lines.append(f"- {risk}")
    if summary.get("notes"):
        lines.append("")
        lines.append("## Notes")
        lines.append(str(summary.get("notes")))

    lines.append("")
    if metadata:
        lines.append("## Metadata Pack")
        lines.append(f"- Custom fields: {len(metadata.get('custom_fields') or [])}")
        lines.append(f"- Validation rules: {len(metadata.get('validation_rules') or [])}")
        lines.append(f"- Permission sets: {len(metadata.get('permission_sets') or [])}")
    if hubspot_pack_data:
        lines.append("## HubSpot Pack")
        lines.append(f"- Properties: {len(hubspot_pack_data.get('properties') or [])}")
        lines.append(f"- Pipelines: {len(hubspot_pack_data.get('pipelines') or [])}")
        lines.append(f"- Workflows: {len(hubspot_pack_data.get('workflow_templates') or [])}")

    matching = proposal.get("matching_config") or {}
    if matching:
        lines.append("")
        lines.append("## Matching Config")
        for key, value in matching.items():
            lines.append(f"- {key}: {value}")

    return "\n".join(lines)


def sanitize_artifact_text(text: str) -> str:
    if not text:
        return text
    redacted = EMAIL_PATTERN.sub("<redacted-email>", text)
    redacted = SFID_PATTERN.sub("<redacted-id>", redacted)
    redacted = USERNAME_PATTERN.sub(r"\1<redacted-user>", redacted)
    return redacted


def sanitize_artifact_json(payload: Any) -> Any:
    if isinstance(payload, dict):
        sanitized = {}
        for key, value in payload.items():
            lowered = key.lower()
            if lowered in {"username", "user", "user_id", "owner_id"} and isinstance(value, str):
                sanitized[key] = "<redacted-user>"
                continue
            sanitized[key] = sanitize_artifact_json(value)
        return sanitized
    if isinstance(payload, list):
        return [sanitize_artifact_json(item) for item in payload]
    if isinstance(payload, str):
        return sanitize_artifact_text(payload)
    return payload


def _ensure_safe_pack_dir(pack_dir: Path, project_dir: Path) -> None:
    resolved_pack = pack_dir.resolve()
    resolved_project = project_dir.resolve()
    try:
        resolved_pack.relative_to(resolved_project)
    except Exception as exc:
        raise ValueError(f"Metadata pack dir must be within project_dir: {resolved_pack}") from exc


def _hash_payload(payload: dict[str, Any]) -> str:
    raw = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
    return f"sha256:{hashlib.sha256(raw.encode('utf-8')).hexdigest()}"


def _write_custom_fields(pack_dir: Path, fields: list[dict[str, Any]]) -> None:
    root = pack_dir / "force-app" / "main" / "default" / "objects"
    for field in fields:
        obj = field.get("object") or "Lead"
        api_name = _safe_api_name(
            field.get("api_name") or field.get("label") or "Custom_Field",
            append_custom_suffix=True,
        )
        path = root / obj / "fields" / f"{api_name}.field-meta.xml"
        path.parent.mkdir(parents=True, exist_ok=True)
        label = field.get("label") or api_name
        field_type = field.get("type") or "Text"
        length = field.get("length")
        required = "true" if field.get("required") else "false"
        description = field.get("description") or ""
        xml = [
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
            "<CustomField xmlns=\"http://soap.sforce.com/2006/04/metadata\">",
            f"  <fullName>{api_name}</fullName>",
            f"  <label>{label}</label>",
            f"  <type>{field_type}</type>",
            f"  <required>{required}</required>",
        ]
        if length and str(length).isdigit():
            xml.append(f"  <length>{int(length)}</length>")
        if description:
            xml.append(f"  <description>{description}</description>")
        xml.append("</CustomField>")
        path.write_text("\n".join(xml), encoding="utf-8")


def _write_validation_rules(pack_dir: Path, rules: list[dict[str, Any]]) -> None:
    root = pack_dir / "force-app" / "main" / "default" / "objects"
    for rule in rules:
        obj = rule.get("object") or "Lead"
        api_name = _safe_api_name(
            rule.get("api_name") or rule.get("label") or "Validation_Rule",
            append_custom_suffix=False,
        )
        path = root / obj / "validationRules" / f"{api_name}.validationRule-meta.xml"
        path.parent.mkdir(parents=True, exist_ok=True)
        active = "true" if rule.get("active", True) else "false"
        condition = rule.get("error_condition") or "FALSE"
        message = rule.get("error_message") or "Validation failed."
        description = rule.get("description") or ""
        xml = [
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
            "<ValidationRule xmlns=\"http://soap.sforce.com/2006/04/metadata\">",
            f"  <fullName>{api_name}</fullName>",
            f"  <active>{active}</active>",
            f"  <errorConditionFormula>{condition}</errorConditionFormula>",
            f"  <errorMessage>{message}</errorMessage>",
        ]
        if description:
            xml.append(f"  <description>{description}</description>")
        xml.append("</ValidationRule>")
        path.write_text("\n".join(xml), encoding="utf-8")


def _write_permission_sets(pack_dir: Path, permsets: list[dict[str, Any]]) -> None:
    root = pack_dir / "force-app" / "main" / "default" / "permissionsets"
    for perm in permsets:
        api_name = _safe_api_name(
            perm.get("api_name") or perm.get("label") or "L2A_Permission",
            append_custom_suffix=False,
        )
        path = root / f"{api_name}.permissionset-meta.xml"
        label = perm.get("label") or api_name
        xml = [
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
            "<PermissionSet xmlns=\"http://soap.sforce.com/2006/04/metadata\">",
            f"  <label>{label}</label>",
        ]
        for fp in perm.get("field_permissions") or []:
            obj = fp.get("object") or "Lead"
            field_name = fp.get("field") or ""
            if not field_name:
                continue
            readable = "true" if fp.get("readable", True) else "false"
            editable = "true" if fp.get("editable", False) else "false"
            xml.extend(
                [
                    "  <fieldPermissions>",
                    f"    <field>{obj}.{field_name}</field>",
                    f"    <readable>{readable}</readable>",
                    f"    <editable>{editable}</editable>",
                    "  </fieldPermissions>",
                ]
            )
        xml.append("</PermissionSet>")
        path.write_text("\n".join(xml), encoding="utf-8")


def _safe_api_name(value: str, *, append_custom_suffix: bool) -> str:
    text = re.sub(r"[^A-Za-z0-9_]", "_", str(value)).strip("_")
    if not text:
        text = "Custom_Field"
    if append_custom_suffix and not text.endswith("__c"):
        text = f"{text}__c"
    return text


__all__ = [
    "generate_proposal",
    "generate_skeleton",
    "generate_metadata_pack",
    "generate_proposal_markdown",
    "sanitize_artifact_text",
    "sanitize_artifact_json",
]
