"""
Automatic GEO fix generator.

Analyzes an AuditResult and generates corrective artifacts:
- robots.txt with missing AI bots
- Complete llms.txt via sitemap
- Missing JSON-LD schemas (WebSite, FAQPage, Organization)
- Missing HTML meta tags

All functions return dataclasses (FixItem/FixPlan), NEVER print.
"""

from __future__ import annotations

import copy
import html as html_lib
import json
import logging
from urllib.parse import urlparse

from geo_optimizer.models.config import AI_BOTS, SCHEMA_TEMPLATES
from geo_optimizer.models.results import AuditResult, FixItem, FixPlan

logger = logging.getLogger(__name__)


def generate_robots_fix(result: AuditResult, base_url: str) -> FixItem | None:
    """Generate robots.txt patch with missing AI bots.

    If robots.txt not found: generates complete file with all 14 bots.
    If found but with missing bots: generates lines to append.

    Returns:
        FixItem or None if no fix needed.
    """
    if result.robots.found and not result.robots.bots_missing and not result.robots.bots_blocked:
        return None

    if not result.robots.found:
        # Generate complete robots.txt
        lines = [
            "# Robots.txt — optimized for AI search engines",
            "# Generated by GEO Optimizer (https://github.com/auriti-labs/geo-optimizer-skill)",
            "",
            "User-agent: *",
            "Allow: /",
            "",
        ]
        for bot, description in AI_BOTS.items():
            lines.append(f"# {description}")
            lines.append(f"User-agent: {bot}")
            lines.append("Allow: /")
            lines.append("")

        # Sitemap placeholder
        parsed = urlparse(base_url)
        lines.append(f"Sitemap: {parsed.scheme}://{parsed.netloc}/sitemap.xml")
        lines.append("")

        return FixItem(
            category="robots",
            description=f"Create robots.txt with access for all {len(AI_BOTS)} AI bots",
            content="\n".join(lines),
            file_name="robots.txt",
            action="create",
        )

    # robots.txt exists but some bots are missing or blocked
    # Fix #211: include anche bots_blocked — un bot bloccato con Disallow
    # necessita ugualmente di una regola Allow per essere accessibile
    bots_to_fix = result.robots.bots_missing + result.robots.bots_blocked
    lines = [
        "",
        "# ─── Missing AI bots (added by GEO Optimizer) ───",
        "",
    ]
    for bot in bots_to_fix:
        description = AI_BOTS.get(bot, "AI bot")
        lines.append(f"# {description}")
        lines.append(f"User-agent: {bot}")
        lines.append("Allow: /")
        lines.append("")

    return FixItem(
        category="robots",
        description=f"Add {len(bots_to_fix)} missing/blocked AI bots to robots.txt",
        content="\n".join(lines),
        file_name="robots.txt",
        action="append",
    )


def generate_llms_fix(result: AuditResult, base_url: str) -> FixItem | None:
    """Generate llms.txt using the site's sitemap.

    If llms.txt not found or incomplete: generates complete version.

    Returns:
        FixItem or None if llms.txt is already complete.
    """
    if result.llms.found and result.llms.has_h1 and result.llms.has_sections and result.llms.has_links:
        return None

    from geo_optimizer.core.llms_generator import discover_sitemap, fetch_sitemap, generate_llms_txt

    # Try to discover the sitemap
    sitemap_url = discover_sitemap(base_url)
    urls = []
    if sitemap_url:
        urls = fetch_sitemap(sitemap_url)

    # Generate llms.txt content
    parsed = urlparse(base_url)
    site_name = parsed.netloc.replace("www.", "")
    content = generate_llms_txt(
        base_url=base_url,
        urls=urls,
        site_name=site_name,
        description=f"Information about {site_name} for AI search engines",
        fetch_titles=False,
        max_urls_per_section=20,
    )

    action = "create" if not result.llms.found else "overwrite"
    desc = "Create llms.txt" if not result.llms.found else "Regenerate llms.txt with complete structure"

    if urls:
        desc += f" ({len(urls)} URL dal sitemap)"

    return FixItem(
        category="llms",
        description=desc,
        content=content,
        file_name="llms.txt",
        action=action,
    )


def generate_schema_fix(result: AuditResult, base_url: str) -> list[FixItem]:
    """Generate missing JSON-LD schemas.

    Checks WebSite, FAQPage and Organization. For each missing type,
    generates the template populated with available data.

    Returns:
        List of FixItem (may be empty).
    """
    fixes = []
    parsed = urlparse(base_url)
    site_name = parsed.netloc.replace("www.", "")

    # WebSite schema
    if not result.schema.has_website:
        template = copy.deepcopy(SCHEMA_TEMPLATES["website"])
        from geo_optimizer.core.schema_injector import fill_template

        values = {
            "name": result.meta.title_text or site_name,
            "url": base_url,
            "description": result.meta.description_text or f"Website {site_name}",
        }
        schema = fill_template(template, values)
        fixes.append(
            FixItem(
                category="schema",
                description="Generate WebSite JSON-LD schema",
                content=json.dumps(schema, indent=2, ensure_ascii=False),
                file_name="schema-website.jsonld",
                action="snippet",
            )
        )

    # Organization schema
    if "Organization" not in result.schema.found_types:
        template = copy.deepcopy(SCHEMA_TEMPLATES["organization"])
        from geo_optimizer.core.schema_injector import fill_template

        values = {
            "name": result.meta.title_text or site_name,
            "url": base_url,
            "description": result.meta.description_text or "",
            "logo_url": f"{base_url}/logo.png",
        }
        schema = fill_template(template, values)
        fixes.append(
            FixItem(
                category="schema",
                description="Generate Organization JSON-LD schema",
                content=json.dumps(schema, indent=2, ensure_ascii=False),
                file_name="schema-organization.jsonld",
                action="snippet",
            )
        )

    return fixes


def generate_meta_fix(result: AuditResult, base_url: str) -> FixItem | None:
    """Generate missing HTML meta tags.

    Checks title, description, canonical, Open Graph and generates
    missing tags as HTML snippets to insert in the <head>.

    Returns:
        FixItem or None if all meta tags are present.
    """
    parsed = urlparse(base_url)
    site_name = parsed.netloc.replace("www.", "")
    tags = []

    # Escape all values derived from the target site to prevent
    # HTML injection when the user inserts the snippet in their <head> (fix #180)
    safe_site = html_lib.escape(site_name, quote=True)
    safe_url = html_lib.escape(base_url, quote=True)

    if not result.meta.has_title:
        tags.append(f"<title>{safe_site}</title>")

    if not result.meta.has_description:
        tags.append(f'<meta name="description" content="Website {safe_site}">')

    if not result.meta.has_canonical:
        tags.append(f'<link rel="canonical" href="{safe_url}/">')

    if not result.meta.has_og_title:
        title = html_lib.escape(result.meta.title_text or site_name, quote=True)
        tags.append(f'<meta property="og:title" content="{title}">')

    if not result.meta.has_og_description:
        desc = html_lib.escape(result.meta.description_text or f"Website {site_name}", quote=True)
        tags.append(f'<meta property="og:description" content="{desc}">')

    if not result.meta.has_og_image:
        tags.append(f'<meta property="og:image" content="{safe_url}/og-image.png">')

    if not tags:
        return None

    return FixItem(
        category="meta",
        description=f"Generate {len(tags)} missing meta tags",
        content="\n".join(tags),
        file_name="meta-tags.html",
        action="snippet",
    )


def generate_ai_discovery_fix(result: AuditResult, base_url: str) -> list[FixItem]:
    """Genera template per endpoint AI discovery mancanti (geo-checklist.dev).

    Genera /ai/summary.json e /ai/faq.json se assenti.
    NON genera /.well-known/ai.txt (troppo specifico per il server).

    Returns:
        Lista di FixItem (può essere vuota).
    """
    fixes = []
    parsed = urlparse(base_url)
    site_name = parsed.netloc.replace("www.", "")

    # Template /ai/summary.json
    if not result.ai_discovery.has_summary or not result.ai_discovery.summary_valid:
        summary_data = {
            "name": result.meta.title_text or site_name,
            "description": result.meta.description_text or f"Website {site_name}",
            "url": base_url,
            "lastModified": "{{ISO_DATE}}",
        }
        fixes.append(
            FixItem(
                category="ai_discovery",
                description="Genera /ai/summary.json con informazioni del sito per AI",
                content=json.dumps(summary_data, indent=2, ensure_ascii=False),
                file_name="ai/summary.json",
                action="create",
            )
        )

    # Template /ai/faq.json
    if not result.ai_discovery.has_faq:
        faq_data = {
            "faqs": [
                {
                    "question": f"What is {site_name}?",
                    "answer": result.meta.description_text or f"A website about {site_name}.",
                },
                {
                    "question": f"How can I use {site_name}?",
                    "answer": f"Visit {base_url} to get started.",
                },
            ]
        }
        fixes.append(
            FixItem(
                category="ai_discovery",
                description="Genera /ai/faq.json con FAQ di esempio per AI",
                content=json.dumps(faq_data, indent=2, ensure_ascii=False),
                file_name="ai/faq.json",
                action="create",
            )
        )

    return fixes


def _estimate_score_after(result: AuditResult, fixes: list[FixItem]) -> int:
    """Estimate the score after applying the fixes.

    Calculates the delta as the difference between the maximum achievable score
    and the current score for each fixed category (fix #187).
    """
    from geo_optimizer.models.config import SCORING

    bonus = 0
    categories_fixed = {f.category for f in fixes}

    if "robots" in categories_fixed:
        # Delta = maximum possible - current (fix #187, #203: inline to avoid core→cli import)
        max_robots = SCORING["robots_found"] + SCORING["robots_citation_ok"]
        current_robots = 0
        if result.robots.found:
            current_robots = SCORING["robots_found"]
            if result.robots.citation_bots_ok:
                current_robots += (
                    SCORING["robots_citation_ok"]
                    if result.robots.citation_bots_explicit
                    else SCORING["robots_some_allowed"]
                )
            elif result.robots.bots_allowed:
                current_robots += SCORING["robots_some_allowed"]
        bonus += max_robots - current_robots

    if "llms" in categories_fixed and not result.llms.found:
        bonus += SCORING["llms_found"] + SCORING["llms_h1"] + SCORING["llms_sections"] + SCORING["llms_links"]

    if "schema" in categories_fixed and not result.schema.has_website:
        bonus += SCORING["schema_website"]

    if "meta" in categories_fixed:
        if not result.meta.has_title:
            bonus += SCORING["meta_title"]
        if not result.meta.has_description:
            bonus += SCORING["meta_description"]
        if not result.meta.has_canonical:
            bonus += SCORING["meta_canonical"]
        if not result.meta.has_og_title and not result.meta.has_og_description:
            bonus += SCORING["meta_og"]

    if "ai_discovery" in categories_fixed:
        if not result.ai_discovery.has_summary or not result.ai_discovery.summary_valid:
            bonus += SCORING["ai_discovery_summary"]
        if not result.ai_discovery.has_faq:
            bonus += SCORING["ai_discovery_faq"]

    return min(100, result.score + bonus)


def run_all_fixes(
    url: str,
    audit_result: AuditResult | None = None,
    only: set[str] | None = None,
) -> FixPlan:
    """Orchestrator: runs audit if needed, generates all fixes.

    Args:
        url: Site URL.
        audit_result: Pre-computed AuditResult (avoids double audit).
        only: Set of categories to generate (e.g. {"robots", "llms"}).
              If None, generates all applicable fixes.

    Returns:
        FixPlan with all generated fixes and estimated score.
    """
    if audit_result is None:
        from geo_optimizer.core.audit import run_full_audit

        audit_result = run_full_audit(url)

    # Normalize URL
    base_url = audit_result.url

    fixes: list[FixItem] = []
    skipped: list[str] = []
    all_categories = {"robots", "llms", "schema", "meta", "ai_discovery"}
    active = only if only else all_categories

    # Robots fix
    if "robots" in active:
        fix = generate_robots_fix(audit_result, base_url)
        if fix:
            fixes.append(fix)
        else:
            skipped.append("robots: all AI bots are already allowed")
    else:
        skipped.append("robots: excluded by --only filter")

    # LLMS fix
    if "llms" in active:
        fix = generate_llms_fix(audit_result, base_url)
        if fix:
            fixes.append(fix)
        else:
            skipped.append("llms: llms.txt is already complete")
    else:
        skipped.append("llms: excluded by --only filter")

    # Schema fix
    if "schema" in active:
        schema_fixes = generate_schema_fix(audit_result, base_url)
        fixes.extend(schema_fixes)
        if not schema_fixes:
            skipped.append("schema: all relevant schemas are present")
    else:
        skipped.append("schema: excluded by --only filter")

    # AI Discovery fix (v4.1: genera template per endpoint AI discovery)
    if "ai_discovery" in active:
        ai_fixes = generate_ai_discovery_fix(audit_result, base_url)
        fixes.extend(ai_fixes)
        if not ai_fixes:
            skipped.append("ai_discovery: tutti gli endpoint AI discovery sono presenti")
    else:
        skipped.append("ai_discovery: excluded by --only filter")

    # Meta fix
    if "meta" in active:
        fix = generate_meta_fix(audit_result, base_url)
        if fix:
            fixes.append(fix)
        else:
            skipped.append("meta: all meta tags are present")
    else:
        skipped.append("meta: excluded by --only filter")

    score_after = _estimate_score_after(audit_result, fixes)

    return FixPlan(
        url=base_url,
        score_before=audit_result.score,
        score_estimated_after=score_after,
        fixes=fixes,
        skipped=skipped,
    )
