# copyright 2012-2022 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr -- mailto:contact@logilab.fr
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.

"""cubicweb-seo views/forms/actions/components for web ui"""

from io import StringIO
from datetime import date
import gzip
import os
import tempfile
import urllib.parse
import zipfile

from cubicweb import Unauthorized
from cubicweb_web.view import View
from cubicweb.predicates import none_rset
from cubicweb.appobject import AppObject
from cubicweb_web import httpcache
from cubicweb_web.views.urlrewrite import SimpleReqRewriter, rgx


class SeoReqRewriter(SimpleReqRewriter):
    rules = [
        (rgx("/robots.txt$"), dict(vid="robotstxt")),
        (rgx("/sitemap.xml$"), dict(vid="sitemapxml")),
    ]


class RobotsRule(AppObject):
    """abstract base class for robots rules

    A custom rule should be built by extending this class. For instance::

      from cubicweb_seo.views import RobotsRule

      class VcsfileRobotsRule(RobotsRule):
          __regid__ = 'vcsfile'
          STANZA = ['User-Agent: *',
                    'Disallow: /versioncontent',
                    'Disallow: /testexecution',
                    'Disallow: /versionedfile',
                    'Disallow: /deletedversioncontent']

    To get more control, override the items() method.
    """

    __registry__ = "robotstxt"
    __abstract__ = True
    STANZA = []

    def stanza(self):
        return "\n".join(self.STANZA)


class CwRobotsRule(RobotsRule):
    __regid__ = "cubicweb.admin"
    STANZA = [
        "User-Agent: *",
        "Disallow: /schema",
        "Disallow: /cwetype",
        "Disallow: /cwrtype",
        "Disallow: /search",
        "Disallow: /login",
        "Disallow: /cwuser",
        "Disallow: /cwgroup",
        "Disallow: /cwsource",
        "Disallow: /_registry",
        "Disallow: /siteconfig",
        "Disallow: /siteinfo",
        "Disallow: /notfound",
        "Disallow: /processinfo",
        "Disallow: /changelog",
        "Disallow: /add/",
    ]


class RobotsTxt(View):
    """A view that implements the http://robotstxt.org standard"""

    __regid__ = "robotstxt"
    content_type = "text/plain"
    templatable = False
    __select__ = none_rset()

    def call(self):
        self.w(f"Sitemap: {self._cw.build_url('sitemap.xml')}\n\n")
        if (
            "robotstxt" in self._cw.vreg
        ):  # XXX or make sure it exists by creating it in this cube...
            for rule in self._cw.vreg["robotstxt"].possible_objects(self._cw):
                self.w(rule.stanza())
                self.w("\n")


CHANGEFREQS = frozenset(
    ("always", "hourly", "daily", "weekly", "monthly", "yearly", "never")
)


class SitemapRule(AppObject):
    """abstract base class for sitemap rules

    A custom rule should be built by extending this class. For instance::

      from cubicweb_seo.views import SitemapRule

      class CardSitemapRule(SitemapRule):
          __regid__ = 'card'
          query = 'Any X WHERE X is Card'
          priority = 1.0
          chfreq = 'monthly'

    To get more control, override the items() method.
    """

    __registry__ = "sitemap"
    __abstract__ = True
    query = None
    priority = None  # optional
    chfreq = None  # optional

    def items(self):
        rset = self._cw.execute(self.query)
        for item in rset.entities():
            yield (
                item.absolute_url(),
                item.modification_date,
                self.chfreq,
                self.priority,
            )


class Sitemaps(View):
    """A view that implements the http://sitemaps.org standard

    Content types must append rules to the sitemap registry in order to appear
    in the list at /sitemap.xml.
    """

    __regid__ = "sitemapxml"
    content_type = "text/xml"
    templatable = False
    __select__ = none_rset()

    def add_url(self, url, lastmod=None, chfreq=None, priority=None):
        self.w(f"<url><loc>{url}</loc>")
        if lastmod:
            self.w(f"<lastmod>{lastmod.strftime('%Y-%m-%d')}</lastmod>")
        if chfreq:
            if chfreq in CHANGEFREQS:
                self.w(f"<changefreq>{chfreq}</changefreq>")
            else:
                self.error(
                    "url %s gave %s that is not a valid changefreq according to http://sitemaps.org"
                    % (url, chfreq)
                )
        if priority:
            if 0 <= float(priority) <= 1:
                self.w(f"<priority>{priority:.2f}</priority>")
            else:
                self.error(
                    "url %s gave %s which is not a valid priority according to http://sitemaps.org"
                    % (url, priority)
                )
        self.w("</url>")

    def call(self):
        # XXX have a look at databnf/file/stable/sitemap.py for cases where the sitemap file is
        # too big (>10Mb) or has too many entries (>50k)
        self.w(f'<?xml version="1.0" encoding="{self._cw.encoding}"?>\n')
        self.w('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">')
        self.add_url(self._cw.base_url())
        if (
            "sitemap" in self._cw.vreg
        ):  # XXX or make sure it exists by creating it in this cube...
            count = 0
            for rule in self._cw.vreg["sitemap"].possible_objects(self._cw):
                try:
                    for item in rule.items():
                        self.add_url(*item)
                        count += 1
                        if count > 50000:
                            self.error(
                                "too many items for this sitemap (limit is 50,000)"
                            )
                            break
                    if count > 50000:
                        break
                except Unauthorized as exc:
                    self.info(
                        f"sitemap rule {rule.__regid__} raised Unauthorized: {exc}"
                    )
                except Exception:
                    self.exception(f"sitemap rule {rule.__regid__} raised exception")
        else:
            self.debug("no sitemap registry found")
        self.w("</urlset>")


class GzippedSitemap(Sitemaps):
    """Allows to download a Gzipped sitemap.xml."""

    __regid__ = "sitemapxmlgz"
    content_type = "application/gzip"
    binary = True
    http_cache_manager = httpcache.EntityHTTPCacheManager
    add_to_breadcrumbs = False

    def add_url(self, buf, url, lastmod=None, chfreq=None, priority=None):
        buf.write("<url>")
        buf.write(f"<loc>{urllib.parse.quote(url.encode('utf-8'), safe=':/')}</loc>")
        if lastmod:
            buf.write(f"<lastmod>{lastmod.strftime('%Y-%m-%d')}</lastmod>")
        if chfreq:
            if chfreq in CHANGEFREQS:
                buf.write(f"<changefreq>{chfreq}</changefreq>")
            else:
                self.error(
                    "url %s gave %s which is not a valid changefreq according to "
                    "http://sitemaps.org" % (url, chfreq)
                )
        if priority:
            if 0 <= float(priority) <= 1:
                buf.write(f"<priority>{priority}</priority>")
            else:
                self.error(
                    "url %s gave %s which is not a valid priority according to "
                    "http://sitemaps.org" % (url, priority)
                )
        buf.write("</url>")

    def call(self):
        buf = StringIO()
        buf.write(f'<?xml version="1.0" encoding="{self._cw.encoding}"?>\n')
        buf.write('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">')
        self.add_url(buf, self._cw.base_url())
        if (
            "sitemap" in self._cw.vreg
        ):  # XXX or make sure it exists by creating it in this cube...
            count = 0
            for rule in self._cw.vreg["sitemap"].possible_objects(self._cw):
                try:
                    for item in rule.items():
                        self.add_url(buf, *item)
                        count += 1
                        if count > 50000:
                            self.error(
                                "too many items for this sitemap (limit is 50,000)"
                            )
                            break
                    if count > 50000:
                        break
                except Unauthorized as exc:
                    self.info(
                        f"sitemap rule {rule.__regid__} raised Unauthorized: {exc}"
                    )
                except Exception:
                    self.exception(f"sitemap rule {rule.__regid__} raised exception")
        else:
            self.debug("no sitemap registry found")
        buf.write("</urlset>")
        with tempfile.NamedTemporaryFile(delete=False) as f:
            with gzip.open(f.name, mode="wb") as gz:
                gz.write(buf.getvalue().encode("utf-8"))
        self._cw.set_content_type(
            self.content_type, filename="sitemap.xml.gz", disposition="attachment"
        )
        with open(f.name, "rb") as gz:
            self.w(gz.read())
        os.remove(f.name)


class ZippedMultipleSitemaps(GzippedSitemap):
    """Allows to download a ZIP archive containing multiple Sitemap files and a Sitemap index.

    This view should be used when number of URLs in sitemap will exceed the 50,000 limit.
    Thus URLs must be dispatched among multiple Sitemap files and a Sitemap index file must be
    created.

    Sitemap files will always be gzipped in this case.
    """

    __regid__ = "massive-sitemaps"
    content_type = "application/zip"
    binary = True
    http_cache_manager = httpcache.EntityHTTPCacheManager
    add_to_breadcrumbs = False

    def new_sitemap_buffer(self, index=False):
        """Create a new buffer storing content about a Sitemap file in memory.

        If ``index`` is True, start a buffer for a Sitemap index file."""
        buf = StringIO()
        buf.write(f'<?xml version="1.0" encoding="{self._cw.encoding}"?>\n')
        tagname = "urlset" if not index else "sitemapindex"
        buf.write(f'<{tagname} xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">')
        return buf

    def save_partial_sitemap(self, sitemap_buf, fname, index_buf):
        """Save content in ``sitemap_buf`` into ``fname``, and add an index entry in
        ``index_buf``."""
        sitemap_buf.write("</urlset>")
        with gzip.open(fname, mode="wb") as gz:
            gz.write(sitemap_buf.getvalue().encode("utf-8"))
        index_buf.write("<sitemap>")
        index_buf.write(f"<loc>{self._cw.build_url(os.path.basename(fname))}</loc>")
        index_buf.write(f"<lastmod>{date.today().strftime('%y-%m-%d')}</lastmod>")
        index_buf.write("</sitemap>")

    def save_sitemap_index(self, index_buf, index_fname):
        """Save content in ``sitemap_buf`` into ``fname``, and add an index entry in
        ``index_buf``."""
        index_buf.write("</sitemapindex>")
        with gzip.open(index_fname, mode="wb") as gz:
            gz.write(index_buf.getvalue().encode("utf-8"))

    def call(self):
        index_fname = os.path.join(tempfile.gettempdir(), "sitemap.xml.gz")
        fname_templ = os.path.join(tempfile.gettempdir(), "sitemap{0}.xml.gz")
        buf = self.new_sitemap_buffer()
        index_buf = self.new_sitemap_buffer(index=True)
        self.add_url(buf, self._cw.base_url())
        if (
            "sitemap" in self._cw.vreg
        ):  # XXX or make sure it exists by creating it in this cube...
            entries = file_number = 1
            for rule in self._cw.vreg["sitemap"].possible_objects(self._cw):
                try:
                    for item in rule.items():
                        entries += 1
                        if entries > 50000:
                            self.save_partial_sitemap(
                                buf, fname_templ.format(file_number), index_buf
                            )
                            file_number += 1
                            buf = self.new_sitemap_buffer()
                            entries = 1
                        self.add_url(buf, *item)
                except Unauthorized as exc:
                    self.info(
                        f"sitemap rule {rule.__regid__} raised Unauthorized: {exc}"
                    )
                except Exception:
                    self.exception(f"sitemap rule {rule.__regid__} raised exception")
            self.save_partial_sitemap(buf, fname_templ.format(file_number), index_buf)
            self.save_sitemap_index(index_buf, index_fname)
        else:
            self.debug("no sitemap registry found")
        with tempfile.NamedTemporaryFile(delete=False) as f:
            with zipfile.ZipFile(f.name, mode="w") as zf:
                zf.write(index_fname, os.path.basename(index_fname))
                for i in range(1, file_number + 1):
                    fname = fname_templ.format(i)
                    zf.write(fname, os.path.basename(fname))
        self._cw.set_content_type(
            self.content_type, filename="sitemaps.zip", disposition="attachment"
        )
        with open(f.name, "rb") as zf:
            self.w(zf.read())
        os.remove(f.name)
        for i in range(1, file_number + 1):
            os.remove(fname_templ.format(i))
