#!/usr/bin/env python3
"""
Python Simple Search Script (Yksinkertainen Python-hakuskripti)

KÄYTTÖOHJEET DEBIANILLE JA UBUNTULLE

1. Asenna Pythonin virtuaaliympäristötyökalu (jos sitä ei vielä ole järjestelmässä):
   sudo apt update
   sudo apt install python3-venv

2. Luo työkansio tälle skriptille ja siirry sinne:
   mkdir -p ~/python_haku
   cd ~/python_haku

3. Luo ja aktivoi virtuaaliympäristö (venv):
   python3 -m venv venv
   source venv/bin/activate

4. Asenna tarvittavat Python-paketit virtuaaliympäristöön:
   pip install -U ddgs trafilatura

   - ddgs (DuckDuckGo-hakuihin)
   - trafilatura (verkkosivujen tekstin purkamiseen)

5. Tallenna tämä skripti nimellä python_haku.py kansioon
   ~/python_haku/ ja anna sille suoritusoikeudet:
   chmod +x python_haku.py

KÄYTTÖ

Varmista aina, että olet skriptin sisältävässä kansiossa ja venv on aktivoitu:
   cd ~/python_haku
   source venv/bin/activate

Yksi hakulauseke (tutkimuskysymys):
./python_haku.py \
"Mikä on token tekoälymalleissa"

Useampi hakulauseke yhdessä komennossa:
./python_haku.py \
"Tekoäly keskittyminen jääminen jälkeen" \
--queries \
"Tekoäly taloudellisen vallan keskittyminen" \
"Tekoäly yritysten osaamisen keskittyminen" \
"AI monopoly risks economic political power" \
"AI concentration risks" \
--results 10 \
--max-pages 40

Annettuasi hakukomennon ohjelma pyytää antamaan tulosteelle nimen (ilman .txt-päätettä).
Tulostetiedosto tallennetaan samaan kansioon.

Haku toteutetaan kahdessa vaiheessa:

Vaihe 1 (Phase 1):
    tutkimuskysymys + hakulausekkeet
        -> DDGS-verkkohaku
        -> URL-osoitteiden kaksoiskappaleiden poisto (deduplication)
        -> verkkosivujen lataus
        -> tekstin eristys Trafilatura-kirjastolla
        -> tulosten tallennus tiedostoon pages_index.json

Vaihe 2 (Phase 2):
    pages_index.json -> muunnetaan helppolukuiseksi tekstitiedostoksi (esim. raportti.txt)
"""

import argparse
import json
import re
import sys
import time
from datetime import datetime
from pathlib import Path
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse

from ddgs import DDGS
from trafilatura import extract, extract_metadata, fetch_url

BASE_DIR = Path.home()
OUTPUT_DIR = BASE_DIR / "python_haku"

PAGES_DIR = OUTPUT_DIR / "pages"
SEARCH_DIR = OUTPUT_DIR / "search_rounds"

PAGES_INDEX_JSON = OUTPUT_DIR / "pages_index.json"
PAGES_INDEX_TXT = OUTPUT_DIR / "pages_index.txt"

DEFAULT_RESULTS_PER_QUERY = 10
DEFAULT_MAX_PAGES = 40

MIN_PAGE_CHARS = 500
MAX_PAGE_CHARS = 50000


def log(message):
    print(f"[{datetime.now().strftime('%H:%M:%S')}] {message}", flush=True)


def ensure_directories():
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    PAGES_DIR.mkdir(parents=True, exist_ok=True)
    SEARCH_DIR.mkdir(parents=True, exist_ok=True)


def write_json(path, data):
    path.write_text(
        json.dumps(data, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )


def normalize_url(url):
    """Normalisoi URL-osoite ja poista yleiset seurantaparametrit."""
    try:
        parsed = urlparse(url.strip())

        query_items = parse_qsl(
            parsed.query,
            keep_blank_values=True,
        )

        query_items = [
            (key, value)
            for key, value in query_items
            if not (
                key.lower().startswith("utm_")
                or key.lower() in {
                    "fbclid",
                    "gclid",
                    "mc_cid",
                    "mc_eid",
                }
            )
        ]

        return urlunparse(
            (
                parsed.scheme.lower(),
                parsed.netloc.lower(),
                parsed.path.rstrip("/") or "/",
                "",
                urlencode(query_items),
                "",
            )
        )

    except Exception:
        return url.strip()


def domain_from_url(url):
    try:
        return urlparse(url).netloc.lower().removeprefix("www.")
    except Exception:
        return ""


def search_web(query, results_per_query):
    """Suorita yksi tekstihaku DDGS:n avulla."""
    log(f"Haetaan: {query}")

    try:
        results = DDGS().text(
            query,
            region="us-en",
            safesearch="moderate",
            max_results=results_per_query,
            backend="auto",
        )
    except Exception as exc:
        log(f"Haku epäonnistui: {exc}")
        return []

    pages = []

    for result in results or []:
        url = result.get("href") or result.get("url")

        if not url:
            continue

        url = normalize_url(url)

        pages.append(
            {
                "url": url,
                "domain": domain_from_url(url),
                "title": (result.get("title") or "").strip(),
                "snippet": (result.get("body") or "").strip(),
                "search_query": query,
            }
        )

    return pages


def collect_search_results(queries, results_per_query, max_pages):
    """Suorita haut, tallenna raakatulokset ja poista URL-osoitteiden kaksoiskappaleet."""
    all_results = []

    for query in queries:
        all_results.extend(
            search_web(query, results_per_query)
        )

    write_json(
        SEARCH_DIR / "search_results_raw.json",
        all_results,
    )

    log(f"Raakoja hakutuloksia: {len(all_results)}")

    seen_urls = set()
    unique_results = []

    for result in all_results:
        url = result["url"]

        if url in seen_urls:
            continue

        seen_urls.add(url)
        unique_results.append(result)

        if len(unique_results) >= max_pages:
            break

    write_json(
        SEARCH_DIR / "search_results_unique.json",
        unique_results,
    )

    log(f"Uniikkeja sivuja valittu: {len(unique_results)}")

    return unique_results


def fetch_page(search_result):
    """Lataa sivu ja eristä sen pääteksti Trafilaturan avulla."""
    url = search_result["url"]

    try:
        downloaded = fetch_url(url)

        if not downloaded:
            return None

        text = extract(
            downloaded,
            include_comments=False,
            include_tables=True,
            favor_precision=True,
            url=url,
        )

        if not text:
            return None

        text = text.strip()

        if len(text) < MIN_PAGE_CHARS:
            return None

        metadata = extract_metadata(downloaded)

        title = (
            getattr(metadata, "title", None)
            or search_result["title"]
        )

        author = getattr(metadata, "author", None)
        date = getattr(metadata, "date", None)

        if len(text) > MAX_PAGE_CHARS:
            text = text[:MAX_PAGE_CHARS] + "\n\n[TEXT TRUNCATED]"

        return {
            "url": url,
            "domain": search_result["domain"],
            "title": title,
            "author": author,
            "date": date,
            "snippet": search_result["snippet"],
            "search_query": search_result["search_query"],
            "text": text,
            "text_length": len(text),
            "fetched_at": datetime.now().isoformat(),
        }

    except Exception as exc:
        log(f"Ei voitu ladata {url}: {exc}")
        return None


def fetch_pages(search_results):
    """Lataa ja eristä teksti kaikilta valituilta sivuilta."""
    pages = []
    total = len(search_results)

    for number, result in enumerate(search_results, start=1):
        log(
            f"Ladataan sivu {number}/{total}: "
            f"{result['url']}"
        )

        page = fetch_page(result)

        if page is None:
            log("  Käyttökelpoista tekstiä ei saatu eristettyä.")
            continue

        pages.append(page)

        log(
            f"  Eristetty "
            f"{page['text_length']:,} merkkiä."
        )

        time.sleep(0.5)

    return pages


def save_pages_index(research_question, queries, pages):
    """Luo pages_index.json -tiedoston. Tämä päättää vaiheen 1."""
    data = {
        "research_question": research_question,
        "created_at": datetime.now().isoformat(),
        "queries": queries,
        "page_count": len(pages),
        "pages": pages,
    }

    write_json(PAGES_INDEX_JSON, data)

    log(f"Luotu: {PAGES_INDEX_JSON}")
    log(f"Sivuja indeksissä: {len(pages)}")


def clean_text_for_humans(text):
    """
    Valmistele eristetty teksti tekstitiedostoa (esim. pages_index.txt) varten.

    Kirjaimelliset '\\n'-sekvenssit muutetaan aidoiksi rivinvaihdoiksi.
    Olemassa olevat rivinvaihdot normalisoidaan.
    Liiallisia tyhjiä rivejä vähennetään.
    """
    if not text:
        return ""

    text = text.replace("\\n", "\n")
    text = text.replace("\r\n", "\n")
    text = text.replace("\r", "\n")

    text = "\n".join(
        line.rstrip()
        for line in text.splitlines()
    )

    text = re.sub(r"\n{4,}", "\n\n\n", text)

    return text.strip()


def convert_json_to_txt():
    """Muunna pages_index.json ihmisluettavaan tekstitiedostomuotoon."""
    if not PAGES_INDEX_JSON.exists():
        raise FileNotFoundError(
            f"Tiedostoa ei löydy: {PAGES_INDEX_JSON}"
        )

    data = json.loads(
        PAGES_INDEX_JSON.read_text(
            encoding="utf-8"
        )
    )

    lines = []

    lines.extend(
        [
            "=" * 80,
            "PYTHON SIMPLE SEARCH - PAGE INDEX",
            "=" * 80,
            "",
            "Research question:",
            data.get("research_question", ""),
            "",
            "Created:",
            data.get("created_at", ""),
            "",
            "=" * 80,
            "SEARCH QUERIES",
            "=" * 80,
            "",
        ]
    )

    for number, query in enumerate(
        data.get("queries", []),
        start=1,
    ):
        lines.append(f"{number}. {query}")

    lines.extend(
        [
            "",
            "=" * 80,
            "WEB PAGES",
            "=" * 80,
            "",
        ]
    )

    pages = data.get("pages", [])

    for number, page in enumerate(pages, start=1):
        lines.extend(
            [
                "#" * 80,
                f"SOURCE {number}",
                "#" * 80,
                "",
                f"TITLE: {page.get('title', '')}",
                f"URL: {page.get('url', '')}",
                f"DOMAIN: {page.get('domain', '')}",
                f"AUTHOR: {page.get('author') or ''}",
                f"DATE: {page.get('date') or ''}",
                (
                    "SEARCH QUERY: "
                    f"{page.get('search_query', '')}"
                ),
                (
                    "TEXT LENGTH: "
                    f"{page.get('text_length', '')}"
                ),
                "",
                "SEARCH SNIPPET:",
                clean_text_for_humans(
                    page.get("snippet", "")
                ),
                "",
                "-" * 80,
                "PAGE TEXT:",
                "",
                clean_text_for_humans(
                    page.get("text", "")
                ),
                "",
                "=" * 80,
                "",
            ]
        )

    PAGES_INDEX_TXT.write_text(
        "\n".join(lines),
        encoding="utf-8",
    )

    log(f"Luotu: {PAGES_INDEX_TXT}")


def phase1_search(
    research_question,
    queries,
    results_per_query,
    max_pages,
):
    """Vaihe 1: haku -> lataus -> eristys -> pages_index.json."""
    log("VAIHE 1: VERKKOHAKU")
    log(f"Tutkimuskysymys: {research_question}")
    log(f"Hakulausekkeet: {len(queries)}")

    search_results = collect_search_results(
        queries,
        results_per_query,
        max_pages,
    )

    if not search_results:
        raise RuntimeError("Hakutuloksia ei löytynyt.")

    log("Ladataan ja eristetään sivuja...")

    pages = fetch_pages(search_results)

    if not pages:
        raise RuntimeError(
            "Yhtäkään verkkosivua ei saatu eristettyä."
        )

    save_pages_index(
        research_question,
        queries,
        pages,
    )

    log("")
    log("VAIHE 1 VALMIS")
    log(f"Välitiedosto: {PAGES_INDEX_JSON}")


def parse_arguments():
    parser = argparse.ArgumentParser(
        description=(
            "Yksinkertainen verkkohakuputki. "
            "Vaihe 1 luo pages_index.json-tiedoston. "
            "Vaihe 2 luo tekstitiedoston (esim. raportti.txt)."
        )
    )

    parser.add_argument(
        "query",
        nargs="?",
        help="Tutkimuskysymys.",
    )

    parser.add_argument(
        "--queries",
        nargs="+",
        help=(
            "Eksplisiittiset hakulausekkeet. Jos tätä ei anneta, "
            "tutkimuskysymystä käytetään hakuna."
        ),
    )

    parser.add_argument(
        "--results",
        type=int,
        default=DEFAULT_RESULTS_PER_QUERY,
        help=(
            "DDGS-tulosten enimmäismäärä per haku "
            f"(oletus {DEFAULT_RESULTS_PER_QUERY})."
        ),
    )

    parser.add_argument(
        "--max-pages",
        type=int,
        default=DEFAULT_MAX_PAGES,
        help=(
            "Ladattavien uniikkien sivujen enimmäismäärä "
            f"(oletus {DEFAULT_MAX_PAGES})."
        ),
    )

    parser.add_argument(
        "--txt-only",
        action="store_true",
        help=(
            "Ohita verkkohaku ja muunna ainoastaan olemassa oleva "
            "pages_index.json tekstitiedostoksi."
        ),
    )

    return parser.parse_args()


def main():
    global PAGES_INDEX_TXT

    args = parse_arguments()

    report_name = input("Anna raporttitiedoston nimi (ilman .txt-päätettä): ").strip()
    if not report_name:
        report_name = "pages_index"
    if not report_name.endswith(".txt"):
        report_name += ".txt"

    PAGES_INDEX_TXT = OUTPUT_DIR / report_name

    ensure_directories()

    # Vain vaihe 2.
    if args.txt_only:
        log("VAIHE 2: JSON -> TXT")

        try:
            convert_json_to_txt()
        except Exception as exc:
            log(f"VAKAVA VIRHE: {exc}")
            return 1

        log("VAIHE 2 VALMIS")
        return 0

    # Vaihe 1 vaatii tutkimuskysymyksen.
    if not args.query:
        print("Virhe: anna tutkimuskysymys.")
        print("")
        print("Esimerkki:")
        print(
            '  ./python_haku.py '
            '"What is a transformer neural network?"'
        )
        return 1

    if args.results < 1:
        print("Virhe: --results-arvon on oltava vähintään 1.")
        return 1

    if args.max_pages < 1:
        print("Virhe: --max-pages-arvon on oltava vähintään 1.")
        return 1

    research_question = args.query.strip()

    if args.queries:
        queries = [
            q.strip()
            for q in args.queries
            if q.strip()
        ]
    else:
        queries = [research_question]

    try:
        phase1_search(
            research_question,
            queries,
            args.results,
            args.max_pages,
        )

    except KeyboardInterrupt:
        log("Käyttäjän keskeyttämä.")
        return 130

    except Exception as exc:
        log(f"VAKAVA VIRHE: {exc}")
        return 1

    # Vaihe 2 seuraa automaattisesti vaihetta 1.
    log("")
    log("VAIHE 2: JSON -> TXT")

    try:
        convert_json_to_txt()

    except Exception as exc:
        log(f"VAKAVA VIRHE TXT-muunnoksen aikana: {exc}")
        return 1

    log("")
    log("VALMIS")
    log(f"JSON: {PAGES_INDEX_JSON}")
    log(f"TXT:  {PAGES_INDEX_TXT}")

    return 0


if __name__ == "__main__":
    sys.exit(main())
