"""ICP Generator V2 — rich multi-persona ICP generation.

Produces 2-4 ICPs with pain points, fears, barriers, include/exclude
LinkedIn params, and confidence scores.  Ported from the original
AI in Charge ICP Agent, adapted for HeyLead's lightweight MCP architecture.

Phase 1: Enhanced LLM prompt only (no RAG).
Phase 4 will add RAG pipeline integration when company_context is provided.
"""

from __future__ import annotations

import json
import logging
import re
import time
from typing import Any

from .icp_schemas import (
    HeadcountParam,
    IcpResult,
    LinkedinSearchParam,
    SingleIcp,
    TenureParam,
)
from .llm import LLMClient

logger = logging.getLogger(__name__)

# ──────────────────────────────────────────────
# System prompt
# ──────────────────────────────────────────────

ICP_V2_SYSTEM = """\
You are the ICP assistant: a research-grade agent with deep reasoning. \
Your mission is to turn context about a client's business into \
2-4 psychologically rich ICP personas with LinkedIn search parameters \
in strict JSON format aligned with LinkedIn Sales Navigator filters.

You output structured JSON only. No markdown, no explanation."""

# ──────────────────────────────────────────────
# User prompt template
# ──────────────────────────────────────────────

ICP_V2_PROMPT = """\
Create 2-4 Ideal Customer Profiles (ICPs) targeting different but \
FITTING market segments as customers for the sender's business.

## TARGET DESCRIPTION
"{target_description}"

## USER CONTEXT (the sender)
Name: {user_name}
Title: {user_title}
Company: {user_company}
Expertise: {user_expertise}
{company_context_section}
{focus_section}

## QUALITY BARS
* LinkedIn parameter accuracy >= 85% (top results must clearly fit the persona)
* "That's exactly me" resonance >= 60% (qualitative alignment with LinkedIn behavioral indicators)
* Use evidence from context above — cite or abstain if insufficient evidence

## WORKFLOW
1. Intake & scoping: identify product, audience, regions, success definition.
2. Draft ICPs (2-4): apply DNA-of-Client framework \
(Identity/Values -> Pains/Fears/Triggers -> Barriers/Myths -> Transformation -> Decision Mechanics).

## LINKEDIN PARAMETER FORMAT RULES
Strict formatting for each field:
- **locations**: maximum granularity is country, minimum is city. \
Do NOT use regional terms like "EMEA", "APAC", or "North America".
- **job_titles**: use specific but simple wordings. \
Example: "Director of Sales" not "Director of Sales Operations".
- **departments**: short terms, 1-2 words only: "Sales", "Business Development", "Marketing".
- **industries**: 1-3 words maximum: "Staffing", "Coaching", "Consulting".

## TASK
For each ICP, provide:

### Identity
- **name**: Short segment name (e.g., "Enterprise Fintech CTOs")
- **description**: 2-3 sentence description of this target segment

### Buyer Psychology
- **pain_points**: 2-4 specific pain points this persona faces
- **fears**: 2-3 underlying fears driving buying decisions
- **barriers**: 2-3 obstacles preventing them from buying

### LinkedIn Search Parameters
Use include/exclude pattern for precise targeting:
- **industries**: {{"include": ["Financial Services", "Fintech"], "exclude": []}}
- **job_titles**: {{"include": ["CTO", "VP Engineering"], "exclude": ["Intern"]}}
- **locations**: {{"include": ["United States"], "exclude": []}}
- **departments**: {{"include": ["Engineering"], "exclude": []}} or null
- **company_headcount**: {{"min": 51, "max": 500}} \
(valid mins: 1,11,51,201,501,1001,5001,10001; valid maxes: 10,50,200,500,1000,5000,10000)
- **company_types**: list from: self_employed, self_owned, government_agency, \
public_company, privately_held, non_profit, educational_institution, partnership
- **company_locations**: {{"include": [...], "exclude": []}} or null
- **tenure**: {{"min": 1, "max": 5}} (valid mins: 0,1,3,6,10; valid maxes: 1,2,5,10)
- **seniority**: {{"include": ["cxo", "vp", "director"], "exclude": []}} \
(valid values: owner/partner, cxo, vice_president, director, experienced_manager, \
entry_level_manager, strategic, senior, entry_level, in_training)
- **keywords**: list of LinkedIn search keywords

### Confidence
Rate your confidence in this ICP (0.0-1.0):
- **overall_confidence**: How reliable is this ICP overall
- **data_completeness**: How many fields are meaningfully populated
- **evidence_strength**: Quality of supporting evidence (lower if no company context provided)

## RESPONSE FORMAT
Return ONLY valid JSON:
{{
    "summary": "One-line description of the ideal customer",
    "campaign_name": "Short campaign name (3-5 words)",
    "relevance_hook": "Why the sender is credible reaching out to this audience",
    "icps": [
        {{
            "name": "Segment Name",
            "description": "...",
            "pain_points": ["...", "..."],
            "fears": ["...", "..."],
            "barriers": ["...", "..."],
            "industries": {{"include": ["..."], "exclude": []}},
            "job_titles": {{"include": ["..."], "exclude": []}},
            "locations": {{"include": ["..."], "exclude": []}},
            "departments": {{"include": ["..."], "exclude": []}},
            "company_headcount": {{"min": 51, "max": 500}},
            "company_types": ["privately_held"],
            "company_locations": null,
            "tenure": {{"min": 1, "max": 5}},
            "seniority": {{"include": ["cxo", "vp"], "exclude": []}},
            "keywords": ["fintech", "payments"],
            "overall_confidence": 0.75,
            "data_completeness": 0.80,
            "evidence_strength": 0.65
        }}
    ]
}}"""


# ──────────────────────────────────────────────
# Generator
# ──────────────────────────────────────────────

async def generate_icp_v2(
    target_description: str,
    company_context: str = "",
    focus_query: str = "",
    user_profile: dict[str, Any] | None = None,
    confidence_threshold: float = 0.4,
) -> IcpResult:
    """Generate a rich multi-persona ICP with LinkedIn enrichment.

    Flow:
    1. Generate ICP personas via LLM (backend or local)
    2. Filter by confidence threshold
    3. Enrich with LinkedIn parameter codes (industries, locations, titles → codes)

    Args:
        target_description: Natural language target (e.g., "CTOs at fintech startups")
        company_context: URL or text about the sender's company
        focus_query: Optional focus (e.g., "enterprise segment only")
        user_profile: Sender's profile + expertise data
        confidence_threshold: Min confidence to include an ICP (0.0-1.0)

    Returns:
        IcpResult with 2-4 SingleIcp personas, enriched with LinkedIn codes.
    """
    start_time = time.time()

    # Route through backend if in backend mode and no local LLM key
    from ..config import has_local_llm_key, is_backend_mode
    if is_backend_mode() and not has_local_llm_key():
        result = await _generate_via_backend(
            target_description, company_context, focus_query, user_profile,
        )
        result.processing_time = time.time() - start_time
        # Enrich with LinkedIn codes
        await _enrich_result(result)
        return result

    # Build prompt
    user_name = ""
    user_title = ""
    user_company = ""
    user_expertise = ""

    if user_profile:
        user_name = user_profile.get("name", "")
        user_title = user_profile.get("title", "")
        user_company = user_profile.get("company", "")
        user_expertise = user_profile.get(
            "core", user_profile.get("headline", ""),
        )

    company_context_section = ""
    if company_context:
        company_context_section = (
            f"\n## COMPANY CONTEXT\n{company_context[:3000]}\n"
        )

    focus_section = ""
    if focus_query:
        focus_section = f"\n## FOCUS\n{focus_query}\n"

    prompt = ICP_V2_PROMPT.format(
        target_description=target_description,
        user_name=user_name or "Unknown",
        user_title=user_title or "Founder",
        user_company=user_company or "Unknown",
        user_expertise=user_expertise or "Not specified",
        company_context_section=company_context_section,
        focus_section=focus_section,
    )

    client = LLMClient()
    raw = await client.generate(
        prompt, system=ICP_V2_SYSTEM, temperature=0.2, max_tokens=8192,
    )

    result = await _parse_icp_response(raw, target_description)
    result.processing_time = time.time() - start_time

    # Filter below-threshold ICPs
    if confidence_threshold > 0:
        result.icps = [
            icp for icp in result.icps
            if icp.overall_confidence >= confidence_threshold
        ]

    # Ensure at least 1 ICP survives filtering
    if not result.icps:
        logger.warning("All ICPs below confidence threshold, keeping best one")
        all_icps = (await _parse_icp_response(raw, target_description)).icps
        if all_icps:
            best = max(all_icps, key=lambda x: x.overall_confidence)
            result.icps = [best]

    # Enrich with LinkedIn codes
    await _enrich_result(result)

    return result


async def _generate_via_backend(
    target_description: str,
    company_context: str,
    focus_query: str,
    user_profile: dict[str, Any] | None,
) -> IcpResult:
    """Generate ICP via the backend proxy.

    The backend returns v2 format (icps[] with pain_points, fears, barriers).
    Falls back to legacy conversion if the response has segments[] instead.
    """
    from ..linkedin import get_linkedin_client

    client = get_linkedin_client()
    try:
        icp_data = await client.generate_icp(
            target_description, user_profile or {},
        )
    finally:
        await client.close()

    # Backend returns v2 format (icps[]) — parse directly
    if "icps" in icp_data:
        return _dict_to_icp_result(icp_data, target_description)

    # Fallback: legacy format (segments[]) — convert
    return _convert_legacy_icp(icp_data, target_description)


# ──────────────────────────────────────────────
# LinkedIn enrichment (resolves names → codes)
# ──────────────────────────────────────────────

async def _enrich_result(result: IcpResult) -> None:
    """Enrich all ICPs in a result with LinkedIn parameter codes.

    Resolves human-readable values (e.g., "Financial Services") to LinkedIn's
    internal codes via the Unipile search/parameters API. This enables structured
    search instead of keyword-only search.

    Operates in-place on the IcpResult.
    """
    if not result.icps:
        return

    from .linkedin_enricher import enrich_icp_linkedin_params
    from ..linkedin import get_linkedin_client
    from ..db.queries import get_setting

    account_id = get_setting("unipile_account_id")
    if not account_id:
        logger.info("No LinkedIn account — skipping enrichment")
        return

    try:
        client = get_linkedin_client()
    except Exception as e:
        logger.warning(f"Cannot create LinkedIn client for enrichment: {e}")
        return

    # Build the get_params_fn closure based on client type
    from ..linkedin.backend_client import BackendClient

    if isinstance(client, BackendClient):
        async def get_params_fn(type: str, keywords: str) -> list[dict[str, str]]:
            return await client.get_search_params(type=type, keywords=keywords)
    else:
        async def get_params_fn(type: str, keywords: str) -> list[dict[str, str]]:
            return await client.get_search_params(
                account_id=account_id, type=type, keywords=keywords,
            )

    enriched_count = 0
    try:
        for icp in result.icps:
            try:
                icp.linkedin_enriched_params = await enrich_icp_linkedin_params(
                    icp, get_params_fn,
                )
                enriched_count += 1
            except Exception as e:
                logger.warning(f"Enrichment failed for ICP '{icp.name}': {e}")

        if enriched_count:
            logger.info(
                f"Enriched LinkedIn params for {enriched_count}/{len(result.icps)} ICPs"
            )
    finally:
        await client.close()


# ──────────────────────────────────────────────
# Response parsing
# ──────────────────────────────────────────────

async def _parse_icp_response(raw: str, target_description: str) -> IcpResult:
    """Parse LLM JSON response into IcpResult.

    Uses a 4-strategy repair pipeline (ported from original AI in Charge):
    1. Raw parse (strip markdown fences)
    2. Regex cleanup (trailing commas, stray chars, extract JSON object)
    3. LLM repair (send broken JSON + schema to LLM)
    4. Fallback skeleton
    """
    # Strategy 1: strip markdown fences and try raw parse
    cleaned = raw.strip()
    if cleaned.startswith("```"):
        lines = cleaned.split("\n")
        cleaned = "\n".join(
            line for line in lines if not line.strip().startswith("```")
        )

    try:
        data = json.loads(cleaned)
        return _dict_to_icp_result(data, target_description)
    except json.JSONDecodeError:
        pass

    # Strategy 2: regex cleanup — extract JSON object, fix common issues
    try:
        repaired = _repair_json(cleaned)
        data = json.loads(repaired)
        logger.info("ICP JSON recovered via regex repair")
        return _dict_to_icp_result(data, target_description)
    except (json.JSONDecodeError, ValueError):
        pass

    # Strategy 3: LLM repair — send broken JSON + schema to LLM for fixing
    try:
        repaired_data = await _llm_repair_json(cleaned, target_description)
        if repaired_data:
            logger.info("ICP JSON recovered via LLM repair")
            return _dict_to_icp_result(repaired_data, target_description)
    except Exception as e:
        logger.warning("LLM JSON repair failed: %s", e)

    # Strategy 4: fallback skeleton
    logger.warning("Failed to parse ICP V2 JSON after all repair strategies, building fallback")
    return _build_fallback(target_description)


def _repair_json(raw: str) -> str:
    """Attempt to repair malformed JSON from LLM output.

    Handles:
    - Stray text before/after the JSON object
    - Trailing commas before } or ]
    - Single quotes instead of double quotes
    - Unquoted keys
    """
    # Extract the outermost JSON object
    match = re.search(r"\{", raw)
    if not match:
        raise ValueError("No JSON object found")

    # Find matching closing brace
    depth = 0
    start = match.start()
    for i in range(start, len(raw)):
        if raw[i] == "{":
            depth += 1
        elif raw[i] == "}":
            depth -= 1
            if depth == 0:
                candidate = raw[start:i + 1]
                break
    else:
        candidate = raw[start:]

    # Remove trailing commas before } or ]
    candidate = re.sub(r",\s*([}\]])", r"\1", candidate)

    # Remove JavaScript-style comments
    candidate = re.sub(r"//[^\n]*", "", candidate)

    return candidate


async def _llm_repair_json(broken_json: str, target_description: str) -> dict[str, Any] | None:
    """Strategy 3: Send broken JSON to LLM for repair.

    Ported from original AI in Charge graph.py _llm_repair_json.
    Sends the broken JSON plus the expected schema to the LLM,
    asking it to return only fixed JSON.
    """
    repair_prompt = f"""The following JSON is malformed or does not match the required schema. Fix it and return ONLY the valid JSON, nothing else.

Common issues to fix:
- Stray characters before property names
- Missing commas
- Trailing commas before closing brackets
- Truncated content
- Invalid escape sequences
- Fields that violate schema types or required properties

EXPECTED SCHEMA:
{{
    "summary": "string",
    "campaign_name": "string",
    "relevance_hook": "string",
    "icps": [
        {{
            "name": "string",
            "description": "string",
            "pain_points": ["string"],
            "fears": ["string"],
            "barriers": ["string"],
            "industries": {{"include": ["string"], "exclude": ["string"]}},
            "job_titles": {{"include": ["string"], "exclude": ["string"]}},
            "locations": {{"include": ["string"], "exclude": ["string"]}},
            "departments": {{"include": ["string"], "exclude": ["string"]}} or null,
            "company_headcount": {{"min": number, "max": number}} or null,
            "company_types": ["string"] or null,
            "company_locations": {{"include": ["string"], "exclude": ["string"]}} or null,
            "tenure": {{"min": number, "max": number}} or null,
            "seniority": {{"include": ["string"], "exclude": ["string"]}} or null,
            "keywords": ["string"] or null,
            "overall_confidence": number (0-1),
            "data_completeness": number (0-1),
            "evidence_strength": number (0-1)
        }}
    ]
}}

BROKEN JSON:
{broken_json[:6000]}

Return ONLY the fixed JSON. Do not include any explanations, just the JSON itself."""

    try:
        client = LLMClient()
        raw = await client.generate(repair_prompt, temperature=0.0, max_tokens=8192)

        # Clean and parse the repaired JSON
        cleaned = raw.strip()
        if cleaned.startswith("```"):
            lines = cleaned.split("\n")
            cleaned = "\n".join(
                line for line in lines if not line.strip().startswith("```")
            )

        data = json.loads(cleaned)
        if "icps" in data and isinstance(data["icps"], list):
            return data
        return None

    except Exception as e:
        logger.debug("LLM JSON repair inner error: %s", e)
        return None


def _dict_to_icp_result(
    data: dict[str, Any], target_description: str,
) -> IcpResult:
    """Convert a parsed JSON dict into an IcpResult."""
    icps = []
    for icp_data in data.get("icps", []):
        icp = SingleIcp(
            name=icp_data.get("name", ""),
            description=icp_data.get("description", ""),
            pain_points=icp_data.get("pain_points", []),
            fears=icp_data.get("fears", []),
            barriers=icp_data.get("barriers", []),
            industries=_to_search_param(icp_data.get("industries")),
            job_titles=_to_search_param(icp_data.get("job_titles")),
            locations=_to_search_param(icp_data.get("locations")),
            departments=_to_search_param(icp_data.get("departments")),
            company_headcount=HeadcountParam(
                min=_safe_int(icp_data.get("company_headcount", {}).get("min")),
                max=_safe_int(icp_data.get("company_headcount", {}).get("max")),
            ) if isinstance(icp_data.get("company_headcount"), dict) else HeadcountParam(),
            company_types=icp_data.get("company_types", []),
            company_locations=_to_search_param(icp_data.get("company_locations")),
            tenure=TenureParam(
                min=_safe_int(icp_data.get("tenure", {}).get("min")),
                max=_safe_int(icp_data.get("tenure", {}).get("max")),
            ) if isinstance(icp_data.get("tenure"), dict) else TenureParam(),
            seniority=_to_search_param(icp_data.get("seniority")),
            keywords=icp_data.get("keywords", []),
            overall_confidence=float(icp_data.get("overall_confidence", 0.5)),
            data_completeness=float(icp_data.get("data_completeness", 0.5)),
            evidence_strength=float(icp_data.get("evidence_strength", 0.5)),
        )
        icps.append(icp)

    return IcpResult(
        icps=icps,
        summary=data.get("summary", f"Target: {target_description}"),
        campaign_name=data.get("campaign_name", target_description[:30]),
        relevance_hook=data.get("relevance_hook", ""),
    )


def _convert_legacy_icp(
    old_icp: dict[str, Any], target_description: str,
) -> IcpResult:
    """Convert the old-format ICP (from generate_icp v1) to IcpResult.

    Old format has: summary, campaign_name, relevance_hook, segments[]
    where each segment has: name, keywords, titles[], seniority[],
    industries[], company_size[], geography[], estimated_results
    """
    icps = []
    for seg in old_icp.get("segments", []):
        icp = SingleIcp(
            name=seg.get("name", "Primary"),
            description=f"Target: {seg.get('keywords', '')}",
            pain_points=[],
            fears=[],
            barriers=[],
            industries=LinkedinSearchParam(
                include=seg.get("industries", []),
            ),
            job_titles=LinkedinSearchParam(
                include=seg.get("titles", []),
            ),
            locations=LinkedinSearchParam(
                include=seg.get("geography", []),
            ),
            seniority=LinkedinSearchParam(
                include=seg.get("seniority", []),
            ),
            keywords=seg.get("keywords", "").split(",") if isinstance(seg.get("keywords"), str) else [],
            overall_confidence=0.4,
            data_completeness=0.4,
            evidence_strength=0.2,
        )
        icps.append(icp)

    return IcpResult(
        icps=icps,
        summary=old_icp.get("summary", f"Target: {target_description}"),
        campaign_name=old_icp.get("campaign_name", target_description[:30]),
        relevance_hook=old_icp.get("relevance_hook", ""),
    )


def _build_fallback(target_description: str) -> IcpResult:
    """Build a minimal fallback IcpResult when parsing fails."""
    return IcpResult(
        icps=[
            SingleIcp(
                name="Primary",
                description=f"Target: {target_description}",
                keywords=target_description.split()[:5],
                overall_confidence=0.2,
                data_completeness=0.2,
                evidence_strength=0.1,
            ),
        ],
        summary=f"Target: {target_description}",
        campaign_name=target_description[:30],
    )


# ──────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────

def _to_search_param(data: Any) -> LinkedinSearchParam:
    """Convert a dict or None to LinkedinSearchParam."""
    if data is None:
        return LinkedinSearchParam()
    if isinstance(data, dict):
        return LinkedinSearchParam(
            include=data.get("include", []) or [],
            exclude=data.get("exclude", []) or [],
        )
    if isinstance(data, list):
        # Legacy format: bare list → include only
        return LinkedinSearchParam(include=data)
    return LinkedinSearchParam()


def _safe_int(value: Any) -> int | None:
    """Safely convert to int or None."""
    if value is None:
        return None
    try:
        return int(value)
    except (ValueError, TypeError):
        return None
