"""
Schema Injector — Adds JSON-LD schema to HTML pages and generates Astro snippets.

All functions return data; no printing.
"""

from __future__ import annotations

import json
import logging
import re
import shutil

from bs4 import BeautifulSoup

from geo_optimizer.core.schema_validator import validate_jsonld
from geo_optimizer.models.config import SCHEMA_TEMPLATES
from geo_optimizer.models.results import SchemaAnalysis

logger = logging.getLogger(__name__)

ASTRO_TEMPLATE = """\
---
// src/layouts/BaseLayout.astro — GEO-optimized layout
// Generated by GEO Optimizer (github.com/auriti-labs/geo-optimizer-skill)
interface Props {
  title: string;
  description: string;
  url?: string;
  isCalculator?: boolean;
  faqItems?: Array<{ question: string; answer: string }>;
}

const {
  title,
  description,
  url = Astro.url.href,
  isCalculator = false,
  faqItems = [],
} = Astro.props;

const siteUrl = "SITE_URL";
const siteName = "SITE_NAME";

const websiteSchema = {
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": siteName,
  "url": siteUrl,
  "description": description,
};

const webAppSchema = isCalculator ? {
  "@context": "https://schema.org",
  "@type": "WebApplication",
  "name": title,
  "url": url,
  "description": description,
  "applicationCategory": "UtilityApplication",
  "operatingSystem": "Web",
  "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }
} : null;

const faqSchema = faqItems.length > 0 ? {
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": faqItems.map(item => ({
    "@type": "Question",
    "name": item.question,
    "acceptedAnswer": { "@type": "Answer", "text": item.answer }
  }))
} : null;
---

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>{title} | {siteName}</title>
  <meta name="description" content={description} />
  <link rel="canonical" href={url} />

  <!-- Open Graph -->
  <meta property="og:title" content={title} />
  <meta property="og:description" content={description} />
  <meta property="og:url" content={url} />
  <meta property="og:type" content="website" />

  <!-- GEO: Schema JSON-LD -->
  <!-- Security: .replace() previene XSS da chiusura prematura del tag script.
       In Astro/JSX, la regex matcha e sostituisce la sequenza nel JSON serializzato. -->
  <script type="application/ld+json" set:html={JSON.stringify(websiteSchema).replace(/<\\//g, '<\\/')} />
  {webAppSchema && <script type="application/ld+json" set:html={JSON.stringify(webAppSchema).replace(/<\\//g, '<\\/')} />}
  {faqSchema && <script type="application/ld+json" set:html={JSON.stringify(faqSchema).replace(/<\\//g, '<\\/')} />}
</head>
"""


def fill_template(template: dict, values: dict) -> dict:
    """Replace {{key}} placeholders in the template with safe values.

    Values are serialized with json.dumps to prevent
    JSON injection via special characters (quotes, backslashes).

    After substitution, logs a warning if any unsubstituted placeholders remain
    (prevents injection from incomplete templates #114).
    """
    template_str = json.dumps(template)
    for key, value in values.items():
        safe_value = str(value) if value else ""
        # Safe escape: json.dumps adds quotes, we remove them
        # but keep the internal escape (", \, newline, etc.)
        escaped = json.dumps(safe_value)[1:-1]
        template_str = template_str.replace(f"{{{{{key}}}}}", escaped)

    # Check for unsubstituted placeholders (#114)
    residui = re.findall(r"\{\{\w+\}\}", template_str)
    if residui:
        logger.warning(
            "Unsubstituted placeholders in template: %s",
            ", ".join(residui),
        )

    return json.loads(template_str)


def schema_to_html_tag(schema_dict: dict) -> str:
    """Convert a schema dict into an HTML JSON-LD script tag.

    Escapes '</' to prevent XSS: the browser would prematurely close
    the <script> tag if it encounters '</script>' in the JSON.
    """
    json_str = json.dumps(schema_dict, indent=2, ensure_ascii=False)
    # Prevent premature closing of the <script> tag (XSS)
    json_str = json_str.replace("</", r"<\/")
    return f'<script type="application/ld+json">\n{json_str}\n</script>'


def extract_faq_from_html(soup: BeautifulSoup) -> list[dict[str, str]]:
    """
    Auto-extract FAQ items from HTML.

    Looks for common patterns:
    - <dt>question</dt><dd>answer</dd>
    - <details><summary>Q</summary>A</details>
    - <div class="faq-item"><h3>Q</h3><p>A</p></div>
    """
    faqs = []

    # Pattern 1: <dt> and <dd>
    for dt in soup.find_all("dt"):
        dd = dt.find_next_sibling("dd")
        if dd:
            question = dt.get_text(strip=True)
            answer = dd.get_text(strip=True)
            if question and answer and len(question) > 5 and len(answer) > 10:
                faqs.append({"question": question, "answer": answer})

    # Pattern 2: <details> / <summary>
    # Note: do NOT use .extract() to avoid mutating the caller's tree
    for detail in soup.find_all("details"):
        summary = detail.find("summary")
        if summary:
            question = summary.get_text(strip=True)
            # Extract answer without mutating the tree: full text minus the question
            full_text = detail.get_text(strip=True)
            answer = full_text.replace(question, "", 1).strip()
            if question and answer and len(question) > 5 and len(answer) > 10:
                faqs.append({"question": question, "answer": answer})

    # Pattern 3: Common FAQ class patterns
    faq_containers = soup.find_all(class_=re.compile(r"faq|question|qa", re.I))
    for container in faq_containers:
        q_elem = container.find(["h3", "h4", "strong"]) or container.find(class_=re.compile(r"question", re.I))
        if q_elem:
            question = q_elem.get_text(strip=True)
            full_text = container.get_text(strip=True)
            answer = full_text.replace(question, "", 1).strip()
            if question and answer and len(question) > 5 and len(answer) > 10:
                faqs.append({"question": question, "answer": answer})

    return faqs


def analyze_html_file(file_path: str) -> SchemaAnalysis:
    """Analyze an HTML file and return found/missing schemas + extracted data.

    Validates file path against traversal attacks and encoding issues (fix #188).
    """
    from geo_optimizer.utils.validators import validate_safe_path

    safe, path_err = validate_safe_path(file_path, allowed_extensions={".html", ".htm"}, must_exist=True)
    if not safe:
        raise ValueError(f"Invalid file path: {path_err}")

    try:
        with open(file_path, encoding="utf-8") as f:
            content = f.read()
    except UnicodeDecodeError as exc:
        raise ValueError(f"Cannot decode file (unsupported encoding): {file_path}") from exc

    soup = BeautifulSoup(content, "html.parser")
    found_schemas = []
    scripts = soup.find_all("script", type="application/ld+json")

    for idx, script in enumerate(scripts):
        try:
            script_content = script.string
            if script_content:
                data = json.loads(script_content.strip())
                if isinstance(data, list):
                    for item in data:
                        schema_type = item.get("@type", "Unknown")
                        found_schemas.append({"type": schema_type, "data": item, "index": idx})
                else:
                    schema_type = data.get("@type", "Unknown")
                    found_schemas.append({"type": schema_type, "data": data, "index": idx})
        except json.JSONDecodeError as exc:
            # Log at debug — does not block analysis of other scripts (#119)
            logger.debug("Invalid JSON-LD in script #%d: %s", idx, exc)

    found_types = [s["type"] for s in found_schemas]
    missing = []
    if "WebSite" not in found_types:
        missing.append("website")
    if "WebApplication" not in found_types:
        missing.append("webapp")
    if "FAQPage" not in found_types:
        missing.append("faq")

    extracted_faqs = []
    if "FAQPage" not in found_types:
        extracted_faqs = extract_faq_from_html(soup)

    duplicates = {}
    for schema_type in set(found_types):
        count = found_types.count(schema_type)
        if count > 1:
            duplicates[schema_type] = count

    return SchemaAnalysis(
        found_schemas=found_schemas,
        found_types=found_types,
        missing=missing,
        extracted_faqs=extracted_faqs,
        duplicates=duplicates,
        has_head=bool(soup.find("head")),
        total_scripts=len(scripts),
    )


def generate_faq_schema(faq_items: list[dict[str, str]]) -> dict:
    """Generate FAQPage schema from FAQ items."""
    import copy

    schema = copy.deepcopy(SCHEMA_TEMPLATES["faq"])  # fix #17: deepcopy per sicurezza
    schema["mainEntity"] = [
        {
            "@type": "Question",
            "name": item["question"],
            "acceptedAnswer": {"@type": "Answer", "text": item["answer"]},
        }
        for item in faq_items
    ]
    return schema


def inject_schema_into_html(
    file_path: str,
    schema_dict: dict,
    backup: bool = True,
    validate: bool = True,
) -> tuple[bool, str | None]:
    """
    Inject a schema tag into an HTML file (before </head>).

    Validates the file path against path traversal and restricts
    extensions to .html/.htm to prevent injection into arbitrary files.

    Returns:
        tuple: (success, message) where message is an error/status string
    """
    from geo_optimizer.utils.validators import validate_safe_path

    # Anti-path-traversal validation: only existing HTML files
    safe, path_err = validate_safe_path(
        file_path,
        allowed_extensions={".html", ".htm"},
        must_exist=True,
    )
    if not safe:
        return False, f"Invalid path: {path_err}"

    if validate:
        schema_type_field = schema_dict.get("@type")
        if isinstance(schema_type_field, list):
            schema_type = schema_type_field[0].lower() if schema_type_field else None
        elif isinstance(schema_type_field, str):
            schema_type = schema_type_field.lower()
        else:
            schema_type = None

        is_valid, error_msg = validate_jsonld(schema_dict, schema_type, strict=False)
        if not is_valid:
            return False, f"Schema validation failed: {error_msg}"

    if backup:
        backup_path = f"{file_path}.bak"
        shutil.copy2(file_path, backup_path)

    with open(file_path, encoding="utf-8") as f:
        content = f.read()

    soup = BeautifulSoup(content, "html.parser")
    head = soup.find("head")
    if not head:
        return False, "No <head> tag found in HTML"

    schema_tag = soup.new_tag("script", type="application/ld+json")
    # Escape '</' to prevent XSS from premature closing of the <script> tag
    safe_json = json.dumps(schema_dict, indent=2, ensure_ascii=False).replace("</", r"<\/")
    schema_tag.string = "\n" + safe_json + "\n"
    head.append(schema_tag)

    with open(file_path, "w", encoding="utf-8") as f:
        f.write(str(soup))

    return True, None


def generate_astro_snippet(url: str, name: str) -> str:
    """Generate Astro BaseLayout snippet.

    Sanitizes url and name to prevent injection into the Astro template:
    removes quotes and control characters that could
    break the structure of the generated code.
    """

    # Sanitize: remove characters that can inject Astro/JS code
    def _sanitize(val: str) -> str:
        for ch in ('"', "'", "`", "\\", "${", "}", "<", ">"):
            val = val.replace(ch, "")
        return val

    safe_url = _sanitize(url)
    safe_name = _sanitize(name)
    # Truncate to prevent abuse
    safe_url = safe_url[:200]
    safe_name = safe_name[:100]
    return ASTRO_TEMPLATE.replace("SITE_URL", safe_url).replace("SITE_NAME", safe_name)
