#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ***************************************************************************
# * SPDX-FileCopyrightText: 2024 S. MANKOWSKI stephane@mankowski.fr
# * SPDX-FileCopyrightText: 2024 G. DE BURE support@mankowski.fr
# * SPDX-License-Identifier: GPL-3.0-or-later
# ***************************************************************************
import csv
import enum
import gzip
import re
import sys
import urllib.parse
import urllib.request
from collections.abc import Generator, Iterable
from dataclasses import dataclass
from datetime import date, datetime
from typing import TextIO

BASE_URL = "http://www.boursorama.com/bourse/action/graph/ws/download"
LOOKUP_URL = "https://www.boursorama.com/recherche/"
SYMBOL_RE = re.compile(
    r"/cours/(?:[^/]+/)*?(?P<code>(?:[0-9][a-zA-Z][a-zA-Z0-9]+|[A-Z]{2}[A-Z0-9]{10}))(?:[/?#]|$)"
)
BROKER_SYMBOL_RE = re.compile(r"[0-9][a-zA-Z][a-zA-Z0-9]+")
ISIN_RE = re.compile(r"[A-Z]{2}[A-Z0-9]{10}")
NAVIGATION_RE = re.compile(r"Page \d+")


class Interval(enum.Enum):
    daily = "1d"
    monthly = "1mo"
    weekly = "1wk"


INTERVAL_TO_PERIOD = {Interval.daily: 1, Interval.monthly: 30, Interval.weekly: 7}

# Always ask for 365 entries
MAX_ITEMS = 365
EXPECTED_DATE_FORMAT = "%d/%m/%Y %H:%M"


@dataclass
class TickerValue:
    date: date
    price: float


def decode_response_payload(response) -> str:
    payload = response.read()
    if response.headers.get("Content-Encoding", "").lower() == "gzip":
        try:
            payload = gzip.decompress(payload)
        except OSError:
            pass
    encoding = response.headers.get_content_charset() or "utf8"
    return payload.decode(encoding, errors="ignore")


def ticker_is_direct_symbol(ticker: str) -> bool:
    # Only Boursorama market codes (like 1rPCAC) can be used directly.
    # ISINs still need lookup to resolve to the provider-specific code.
    return BROKER_SYMBOL_RE.fullmatch(ticker) is not None


def extract_symbol_from_payload(payload: str, ticker: str) -> str | None:
    ticker_upper = ticker.upper()
    best_candidate: str | None = None
    best_score: int | None = None
    first_pos: int | None = None

    for match in SYMBOL_RE.finditer(payload):
        candidate = match.group("code")
        candidate_upper = candidate.upper()
        context = payload[max(0, match.start() - 120) : match.end() + 120].upper()

        # Heuristics:
        # - 1rP* symbols are canonical quote pages
        # - exact ticker mentions around the link indicate a likely intended match
        # - prefer earlier matches for stable deterministic behavior
        score = 0
        if candidate_upper.startswith("1RP"):
            score += 8
        if candidate_upper.endswith(ticker_upper):
            score += 4
        if ticker_upper in candidate_upper:
            score += 3
        if re.search(rf"\b{re.escape(ticker_upper)}\b", context):
            score += 6
        if ISIN_RE.fullmatch(candidate_upper):
            score += 2

        if (
            best_score is None
            or score > best_score
            or (score == best_score and (first_pos is None or match.start() < first_pos))
        ):
            best_candidate = candidate
            best_score = score
            first_pos = match.start()

    return best_candidate


def ticker_to_url(ticker: str) -> str:
    if ticker_is_direct_symbol(ticker):
        return ticker

    params = urllib.parse.urlencode(dict(query=ticker))
    request = urllib.request.Request(
        f"{LOOKUP_URL}?{params}",
        headers={
            "Cookie": "_pctx=dummy",
            "User-Agent": "Mozilla/5.0",
        },
    )

    with urllib.request.urlopen(request) as response:
        redirect = response.geturl()
        payload = decode_response_payload(response)

    redirect_symbol = extract_symbol_from_payload(redirect, ticker)
    if redirect_symbol:
        return redirect_symbol

    payload_symbol = extract_symbol_from_payload(payload, ticker)
    if payload_symbol:
        return payload_symbol

    if payload:
        raise ValueError(
            f"Ticker search did not return a valid location: check the symbol '{ticker}'"
        )
    raise ValueError("Ticker search failed: empty response")


def request_one_page(
    interval: Interval,
    symbol: str,
) -> str:
    params: dict[str, str | int] = {
        "symbol": symbol,
        # Always ask for at most 365 entries
        "length": MAX_ITEMS,
        "period": INTERVAL_TO_PERIOD[interval],
    }
    query = urllib.parse.urlencode(params)
    request = urllib.request.Request(
        f"{BASE_URL}?{query}",
        headers={
            "Cookie": "_pctx=dummy",
            "User-Agent": "Mozilla/5.0",
        },
    )
    with urllib.request.urlopen(request) as response:
        return decode_response_payload(response)


def download_history(
    interval: Interval, symbol: str, start_date: date
) -> Generator[TickerValue]:
    response = request_one_page(interval, symbol)
    csv_reader = csv.reader(response.splitlines(), delimiter="\t")
    for idx, line in enumerate(csv_reader):
        # First line is an header
        if idx == 0:
            continue
        if not line:
            continue
        date = datetime.strptime(line[0], EXPECTED_DATE_FORMAT).date()
        if date < start_date:
            continue
        value = float(line[1])
        yield TickerValue(date=date, price=value)


def dump_to_csv(ticker_values: Iterable[TickerValue], output: TextIO):
    writer = csv.writer(output)
    writer.writerow(("date", "price"))
    for value in ticker_values:
        writer.writerow((value.date, value.price))


if __name__ == "__main__":
    symbol = sys.argv[1]
    from_date = date.fromisoformat(sys.argv[2])
    interval = Interval(sys.argv[3])

    actual_symbol = ticker_to_url(symbol)
    results = sorted(
        download_history(interval, actual_symbol, from_date),
        key=lambda x: x.date,
        reverse=True,
    )
    dump_to_csv(results, sys.stdout)
