from __future__ import annotations

import json
from dataclasses import asdict, dataclass, field
from datetime import UTC, date, datetime
from pathlib import Path

import pandas as pd

from psx_signal.config import Settings
from psx_signal.data.artifacts import MarketDataArtifact
from psx_signal.data.diagnostics import detect_discontinuities, reconcile_discontinuities
from psx_signal.data.manifest import generate_manifest
from psx_signal.data.master import build_stock_master, read_stock_metadata
from psx_signal.data.normalization import normalize_equities, normalize_indices
from psx_signal.data.providers import PsxProvider
from psx_signal.data.reference_data import read_constituents, read_corporate_actions
from psx_signal.data.storage import DataConflictError, DatasetStore, RawArtifactStore
from psx_signal.data.validation import DataValidator


PRICE_COLUMNS = ["open", "high", "low", "close", "volume"]


@dataclass
class SyncResult:
    status: str = "SUCCESS"
    processed: list[str] = field(default_factory=list)
    skipped: list[str] = field(default_factory=list)
    failures: list[dict[str, str]] = field(default_factory=list)
    rows_added: int = 0
    index_rows_added: int = 0
    report_path: str = ""
    manifest: dict[str, object] = field(default_factory=dict)


class DataSyncService:
    def __init__(self, settings: Settings) -> None:
        self.settings = settings
        self.provider = PsxProvider(settings.data)
        self.store = DatasetStore(settings.data.root)
        self.raw_store = RawArtifactStore(settings.data.root)

    def backfill(self, start: date, end: date) -> SyncResult:
        artifacts = self.provider.available_artifacts(start, end)
        if self.settings.data.licensed_url_template:
            known = {(item.kind, item.trading_date) for item in artifacts}
            for candidate in pd.bdate_range(start, end):
                for kind in ("equities", "indices"):
                    if (kind, candidate.date()) in known:
                        continue
                    try:
                        artifact = self.provider.fetch_artifact(candidate.date(), kind)
                        if artifact:
                            artifacts.append(artifact)
                    except RuntimeError as exc:
                        # Preserve partial progress; failure enters final report.
                        artifacts.append(MarketDataArtifact(
                            Path(f"unavailable-{candidate.date()}-{kind}"), "failure", str(exc),
                            candidate.date(), "psx",
                        ))
        return self._process(artifacts)

    def sync(self, target_date: date | None = None) -> SyncResult:
        if target_date:
            artifacts: list[MarketDataArtifact] = []
            for kind in ("equities", "indices"):
                try:
                    artifact = self.provider.fetch_artifact(target_date, kind)
                    if artifact:
                        artifacts.append(artifact)
                except RuntimeError as exc:
                    artifacts.append(MarketDataArtifact(
                        Path(f"unavailable-{target_date}-{kind}"), "failure", str(exc), target_date, "psx"
                    ))
        else:
            artifacts = self.provider.available_artifacts()
            if self.settings.data.licensed_url_template:
                existing = self.store.read(self.store.equities_path)
                start = (
                    pd.Timestamp(existing["date"].max()) + pd.Timedelta(days=1)
                    if not existing.empty else pd.Timestamp.today().normalize()
                )
                for candidate in pd.bdate_range(start, pd.Timestamp.today().normalize()):
                    for kind in ("equities", "indices"):
                        try:
                            artifact = self.provider.fetch_artifact(candidate.date(), kind)
                            if artifact:
                                artifacts.append(artifact)
                        except RuntimeError as exc:
                            artifacts.append(MarketDataArtifact(
                                Path(f"unavailable-{candidate.date()}-{kind}"), "failure", str(exc),
                                candidate.date(), "psx",
                            ))
        return self._process([artifact for artifact in artifacts if artifact is not None])

    def _process(self, artifacts: list[MarketDataArtifact]) -> SyncResult:
        result = SyncResult()
        metadata_frames: list[pd.DataFrame] = []
        for artifact in artifacts:
            if artifact.kind == "failure":
                result.failures.append({"artifact": artifact.path.name, "error": artifact.source_reference})
                continue
            try:
                raw_reference = self.raw_store.preserve(artifact)
                if artifact.kind == "indices":
                    indices = normalize_indices(artifact, raw_reference, self.settings.benchmark_symbol)
                    indices = indices[indices["index"] == self.settings.benchmark_symbol]
                    _, added = self.store.upsert(
                        indices, self.store.kse100_path, ["index", "date"], PRICE_COLUMNS,
                        ["date", "index"],
                    )
                    result.index_rows_added += added
                elif artifact.kind == "metadata":
                    metadata_frames.append(read_stock_metadata(str(artifact.path)))
                elif artifact.kind == "corporate_actions":
                    actions = read_corporate_actions(artifact.path)
                    _, added = self.store.upsert(
                        actions, self.store.corporate_actions_path,
                        ["symbol", "action_date", "action_type", "source_reference"],
                        ["ratio", "cash_amount"], ["action_date", "symbol"],
                    )
                    result.rows_added += added
                elif artifact.kind == "constituents":
                    constituents = read_constituents(artifact.path)
                    _, added = self.store.upsert(
                        constituents, self.store.constituents_path,
                        ["effective_date", "symbol", "index"], ["source_reference"],
                        ["effective_date", "index", "symbol"],
                    )
                    result.rows_added += added
                else:
                    equities = normalize_equities(artifact, raw_reference)
                    benchmark_rows = equities[equities["symbol"] == self.settings.benchmark_symbol]
                    equities = equities[equities["symbol"] != self.settings.benchmark_symbol]
                    structural = DataValidator(
                        self.settings.extreme_return_threshold, minimum_history=0
                    ).validate(equities)
                    if any(issue.severity == "error" for issue in structural):
                        raise ValueError(f"Structural validation failed: {[issue.code for issue in structural]}")
                    _, added = self.store.upsert(
                        equities, self.store.equities_path, ["symbol", "date"], PRICE_COLUMNS,
                        ["date", "symbol"],
                    )
                    result.rows_added += added
                    if not benchmark_rows.empty:
                        indices = benchmark_rows.rename(columns={"symbol": "index"})
                        keep = ["date", "index", "open", "high", "low", "close", "volume", "provider", "fetched_at", "source_reference", "raw_artifact"]
                        _, added_index = self.store.upsert(
                            indices[keep], self.store.kse100_path, ["index", "date"], PRICE_COLUMNS,
                            ["date", "index"],
                        )
                        result.index_rows_added += added_index
                result.processed.append(artifact.source_reference)
            except (ValueError, DataConflictError, OSError) as exc:
                result.failures.append({"artifact": artifact.source_reference, "error": str(exc)})

        equities = self.store.read(self.store.equities_path)
        indices = self.store.read(self.store.kse100_path)
        existing_master = self.store.read(self.store.stock_master_path)
        metadata = pd.concat(metadata_frames, ignore_index=True) if metadata_frames else existing_master
        if not equities.empty:
            master = build_stock_master(equities, metadata)
            DatasetStore._atomic_parquet(master, self.store.stock_master_path)
        validator = DataValidator(
            self.settings.extreme_return_threshold,
            self.settings.validation.get("expected_session_coverage", 0.90),
            self.settings.minimum_history,
        )
        benchmark_dates = indices["date"] if not indices.empty else None
        quality_path = Path(self.settings.data.root) / "normalized" / "quality-report.json"
        if equities.empty:
            quality = {
                "valid": False, "status": "NO_DATA", "error_count": 0,
                "warning_count": 0, "info_count": 0, "issues": [],
            }
            quality_path.write_text(json.dumps(quality, indent=2), encoding="utf-8")
        else:
            issues = validator.validate(equities, benchmark_dates)
            quality = validator.report(issues, quality_path)
        actions = self.store.read(self.store.corporate_actions_path)
        discontinuities = reconcile_discontinuities(
            detect_discontinuities(equities, self.settings.extreme_return_threshold), actions
        ) if not equities.empty else pd.DataFrame()
        if not discontinuities.empty:
            DatasetStore._atomic_parquet(
                discontinuities, Path(self.settings.data.root) / "normalized" / "discontinuities.parquet"
            )
        if not artifacts and equities.empty and indices.empty:
            result.status = "NO_DATA"
            result.skipped.append("No official artifacts found in configured inbox or licensed source")
        else:
            result.status = "PARTIAL" if result.failures and result.processed else "FAILED" if result.failures else "SUCCESS"
        latest_sync = {
            "status": result.status, "timestamp": datetime.now(UTC).isoformat(), "provider": "psx",
            "processed": len(result.processed), "failures": len(result.failures),
        }
        result.manifest = generate_manifest(self.store, "psx", quality, latest_sync)
        report_path = Path(self.settings.data.root) / "normalized" / "sync-report.json"
        result.report_path = str(report_path)
        report = {
            **asdict(result), "quality": quality,
            "unresolved_discontinuities": int((~discontinuities.get("action_match", pd.Series(dtype=bool))).sum()) if not discontinuities.empty else 0,
        }
        report_path.write_text(json.dumps(report, indent=2, default=str), encoding="utf-8")
        return result


def data_status(settings: Settings) -> dict[str, object]:
    store = DatasetStore(settings.data.root)
    manifest_path = store.root / "normalized" / "manifest.json"
    if not manifest_path.exists():
        return {"status": "NO_DATA", "message": "No normalized dataset. Place official files in inbox and run sync-data."}
    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    discontinuities_path = store.root / "normalized" / "discontinuities.parquet"
    discontinuities = store.read(discontinuities_path)
    manifest["unresolved_discontinuities"] = int(
        (~discontinuities["action_match"].fillna(False)).sum()
    ) if not discontinuities.empty else 0
    return manifest
